Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,4 @@ playwright-report/

/src/generated/prisma
*.db
.loop/
107 changes: 107 additions & 0 deletions e2e/dashboard-mobile.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/**
* Mobile-viewport e2e checks for the Amazon Ads Dashboard.
*
* Per MOBILE_REDESIGN_PLAN Phase 1 + Phase 4:
* - At <768px the dashboard must render CampaignCard articles, not a <table>
* - KPI grid must collapse to 1 column at <480px
* - No horizontal scroll on the campaign list region
* - Touch targets (44px minimum) on the action buttons when expanded
* - The .skip-link skip-to-main-content link remains functional at all widths
*
* These tests run against the mobile-chromium project (iPhone SE viewport
* 375x800) to mirror the at-risk device class in MOBILE_REDESIGN_PLAN.
*/
import { test, expect } from '@playwright/test';

test.describe('Dashboard @ mobile (375px)', () => {
// First compile in the dev server is slow under cold cache. Bump the
// per-test timeout to 90s so the first test does not race cold-start.
test.setTimeout(90000);
test('renders CampaignCard articles instead of a table', async ({ page }) => {
await page.goto('/dashboard', { waitUntil: 'domcontentloaded' });
await expect(page.locator('h1')).toContainText('Advertising');
await expect(page.locator('.campaign-card-list')).toBeVisible();
const cards = page.locator('.campaign-card');
expect(await cards.count()).toBeGreaterThan(0);
await expect(page.locator('.app-content table')).toHaveCount(0);
});

test('every campaign card shows the campaign name and status', async ({ page }) => {
await page.goto('/dashboard');
const cards = page.locator('.campaign-card');
// Wait for the first card to mount before counting (auto-wait on
// .count() does not exist; await expect.poll is the explicit pattern).
await expect(cards.first()).toBeVisible();
const count = await cards.count();
expect(count).toBeGreaterThan(0);
for (let i = 0; i < count; i++) {
const card = cards.nth(i);
await expect(card.locator('.campaign-card__name')).toBeVisible();
// Status is rendered as a pill ('pill green' / 'pill orange' / etc.).
// Match by accessible text content instead of class.
await expect(card.getByText(/^(Enabled|Paused|Archived|Draft)$/)).toBeVisible();
}
});

test('shows the primary metrics on each card (Spend, Sales, ROAS)', async ({ page }) => {
await page.goto('/dashboard');
const firstCard = page.locator('.campaign-card').first();
await expect(firstCard).toContainText('Spend');
await expect(firstCard).toContainText('Sales');
await expect(firstCard).toContainText('ROAS');
});

test('expand toggle reveals ACOS, CPC, Orders + Pause/Archive', async ({ page }) => {
await page.goto('/dashboard');
const firstCard = page.locator('.campaign-card').first();
const toggle = firstCard.locator('.campaign-card__toggle');
await expect(toggle).toHaveAttribute('aria-expanded', 'false');
await toggle.click();
await expect(toggle).toHaveAttribute('aria-expanded', 'true');
await expect(firstCard.locator('text=CPC')).toBeVisible();
await expect(firstCard.locator('text=ACOS')).toBeVisible();
await expect(firstCard.locator('text=Orders')).toBeVisible();
await expect(firstCard.locator('button:has-text("Pause")')).toBeVisible();
await expect(firstCard.locator('button:has-text("Archive")')).toBeVisible();
});

test('Pause and Archive touch targets are at least 44px tall', async ({ page }) => {
await page.goto('/dashboard');
const firstCard = page.locator('.campaign-card').first();
await firstCard.locator('.campaign-card__toggle').click();
const pauseBtn = firstCard.locator('button:has-text("Pause")');
const pauseBox = await pauseBtn.boundingBox();
expect(pauseBox).not.toBeNull();
expect(pauseBox!.height).toBeGreaterThanOrEqual(44);
const archiveBtn = firstCard.locator('button:has-text("Archive")');
const archiveBox = await archiveBtn.boundingBox();
expect(archiveBox).not.toBeNull();
expect(archiveBox!.height).toBeGreaterThanOrEqual(44);
});

test('no horizontal scroll on the campaign list region', async ({ page }) => {
await page.goto('/dashboard');
const cardList = page.locator('.campaign-card-list').first();
await expect(cardList).toBeVisible();
const box = await cardList.boundingBox();
expect(box).not.toBeNull();
expect(box!.width).toBeLessThanOrEqual(375);
});

test('skip-to-main-content link remains functional', async ({ page }) => {
await page.goto('/dashboard');
const skip = page.locator('a.skip-link');
await expect(skip).toHaveAttribute('href', '#main-content');
const main = page.locator('main#main-content');
await expect(main).toBeVisible();
});

test('KPI grid is single-column at 375px', async ({ page }) => {
await page.goto('/dashboard');
const grid = page.locator('.kpi-grid');
await expect(grid).toBeVisible();
const cols = await grid.evaluate((el) => getComputedStyle(el).gridTemplateColumns);
const colCount = cols.trim().split(/\s+/).length;
expect(colCount).toBe(1);
});
});
2 changes: 1 addition & 1 deletion next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const nextConfig: NextConfig = {
value: [
"default-src 'self'",
"img-src 'self' data: https:",
"script-src 'self' 'unsafe-inline'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval'",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- candidate files ---'
fd -i 'next\.config|webpack|csp|content.security' . || true

printf '%s\n' '--- next.config.ts outline ---'
if [ -f next.config.ts ]; then
  ast-grep outline next.config.ts || true
  wc -l next.config.ts
  sed -n '1,180p' next.config.ts
fi

printf '%s\n' '--- related references ---'
rg -n --hidden -S "unsafe-eval|script-src|Content-Security-Policy|webpack|dev|development" \
  -g '!node_modules' -g '!dist' -g '!build' . | head -300

Repository: projectamazonph/Amazon-ad-console

Length of output: 19989


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- next.config.ts ---'
cat -n next.config.ts 2>/dev/null || true

printf '%s\n' '--- package scripts and config references ---'
for f in package.json .env .env.example; do
  if [ -f "$f" ]; then
    printf '%s\n' "--- $f ---"
    cat -n "$f" | head -220
  fi
done
rg -n --hidden -S "unsafe-eval|script-src|Content-Security-Policy|webpack|dev|development" \
  -g '!node_modules' -g '!dist' -g '!build' . | head -300

Repository: projectamazonph/Amazon-ad-console

Length of output: 23274


🏁 Script executed:

#!/bin/bash
set -eu
test -f next.config.ts && {
  printf '%s\n' '--- matching lines ---'
  rg -n -C 8 --fixed-strings "'unsafe-eval'" next.config.ts
  printf '%s\n' '--- header and condition context ---'
  rg -n -C 20 "headers|Content-Security-Policy|NODE_ENV|development|webpack" next.config.ts
}

Repository: projectamazonph/Amazon-ad-console

Length of output: 3755


Do not ship 'unsafe-eval' in the production CSP.

The CSP applies to /(.*) and is not limited by isProd, so production responses allow eval and Function. If development requires it, add it only to a development-specific CSP.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@next.config.ts` at line 49, Update the CSP configuration in next.config.ts to
remove 'unsafe-eval' from the production script-src policy. If development
requires eval support, include it only in a development-specific CSP while
keeping production responses restricted.

"style-src 'self' 'unsafe-inline'",
"font-src 'self' data:",
"connect-src 'self' https://*.neon.tech",
Expand Down
21 changes: 18 additions & 3 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
testDir: './e2e',
fullyParallel: true,
Expand All @@ -13,10 +12,26 @@ export default defineConfig({
screenshot: 'only-on-failure',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
// Desktop project skips the mobile-only e2e coverage.
testIgnore: /dashboard-mobile/,
},
// Mobile project for MOBILE_REDESIGN_PLAN Phase 1 e2e coverage.
// Uses iPhone SE viewport (375x800) per the plan's critical-device list.
{
name: 'mobile-chromium',
use: { ...devices['iPhone SE'] },
testMatch: /dashboard-mobile/,
Comment on lines +23 to +26

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Align the mobile end-to-end checks with the documented viewport and overflow requirement. The mobile project currently inherits a 320×568 viewport instead of 375×800, and the horizontal-scroll assertion checks only the list bounding-box width, so descendant content can overflow while the test still passes. Override the project viewport to 375×800 and assert scrollWidth <= clientWidth for the list region.

📍 Affects 2 files
  • playwright.config.ts#L23-L26 (this comment)
  • e2e/dashboard-mobile.spec.ts#L82-L89
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@playwright.config.ts` around lines 23 - 26, Update the mobile-chromium
project’s use configuration in the Playwright config to override the iPhone SE
device viewport with 375×800, while preserving the existing device settings and
testMatch pattern.

Apply the same fix in `@e2e/dashboard-mobile.spec.ts` around lines 82 - 89: Update
the assertion to detect actual horizontal scroll overflow.

},
],
webServer: {
command: 'npm run dev',
// Webpack fallback: Next.js 16's default Turbopack panics on the pnpm
// symlinked node_modules layout (Invalid symlink — turbopack-error T1).
// Dev server itself runs via webpack for both desktop and mobile e2e.
// Production builds are unaffected and continue to use Turbopack.
command: 'npm run dev -- --webpack',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 120000,
Expand Down
Loading
Loading