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
12 changes: 8 additions & 4 deletions .github/workflows/visual-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ on:

jobs:
visual-baseline:
strategy:
fail-fast: false
matrix:
shardIndex: [1, 2]
shardTotal: [2]
timeout-minutes: 60
runs-on: ubuntu-latest
continue-on-error: true
Expand All @@ -17,7 +22,7 @@ jobs:
with:
node-version-file: '.nvmrc'
cache: 'npm'

- name: Install dependencies
run: npm ci

Expand All @@ -27,20 +32,19 @@ jobs:
rm -rf playwright-report/
rm -rf test-results/


- name: Install Playwright Browsers
run: npx playwright install --with-deps

- name: Run Playwright Visual Tests
run: npx playwright test e2e/flows/99-auth-ui-baseline.spec.ts e2e/flows/supplier-comparison-visual.spec.ts e2e/flows/31-promoflix-player.spec.ts e2e/tooltips-a11y.spec.ts e2e/optimized-image-visual.spec.ts e2e/routes/app/replenishment-grid-visual.spec.ts e2e/routes/app/novelty-grid-visual.spec.ts
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} e2e/flows/99-auth-ui-baseline.spec.ts e2e/flows/supplier-comparison-visual.spec.ts e2e/flows/31-promoflix-player.spec.ts e2e/tooltips-a11y.spec.ts e2e/optimized-image-visual.spec.ts e2e/routes/app/replenishment-grid-visual.spec.ts e2e/routes/app/novelty-grid-visual.spec.ts e2e/routes/app/novelty-card-variations.spec.ts
env:
CI: true

- name: Upload Test Report
if: always()
uses: actions/upload-artifact@v5
with:
name: playwright-report
name: playwright-report-${{ matrix.shardIndex }}
path: playwright-report/
retention-days: 30

Expand Down
86 changes: 86 additions & 0 deletions e2e/routes/app/novelty-card-variations.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { test, expect } from '@playwright/test';

test.describe('Novelty Card Variations @mobile', () => {
test.beforeEach(async ({ context }) => {
await context.route('**/functions/v1/novelties**', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{
novelty_id: 'var-1',
product_id: 'p-1',
product_name: 'Produto com título extremamente longo que deve ocupar pelo menos três ou quatro linhas no grid para testar o alinhamento e o truncamento de texto',
product_sku: 'SKU-LONG-TITLE',
product_image: 'https://placehold.co/400x400?text=Normal',
base_price: 150.50,
stock_quantity: 100,
stock_status: 'in-stock',
detected_at: new Date().toISOString(),
days_remaining: 30,
supplier_name: 'Fornecedor A',
category_name: 'Categoria Alpha'
},
{
novelty_id: 'var-2',
product_id: 'p-2',
product_name: 'Preço sob consulta',
product_sku: 'SKU-QUERY-PRICE',
product_image: 'https://placehold.co/400x400?text=Price+Query',
base_price: 0,
stock_quantity: 50,
stock_status: 'low-stock',
detected_at: new Date().toISOString(),
days_remaining: 15,
supplier_name: 'Fornecedor B',
category_name: 'Categoria Beta'
},
{
novelty_id: 'var-3',
product_id: 'p-3',
product_name: 'Imagem Ausente',
product_sku: 'SKU-NO-IMAGE',
product_image: null,
base_price: 89.90,
stock_quantity: 0,
stock_status: 'out-of-stock',
detected_at: new Date().toISOString(),
days_remaining: 5,
supplier_name: 'Fornecedor C',
category_name: 'Categoria Gamma'
}
])
});
});
});

test('Card Edge Cases - Visual Consistency', async ({ page }) => {
await page.goto('/novidades');
const grid = page.locator('div[role="list"]');
await grid.waitFor({ state: 'visible' });

await expect(grid).toHaveScreenshot('novelty-card-variations.png', {
maxDiffPixelRatio: 0.05
});

const cards = page.locator('div[role="listitem"]');
const count = await cards.count();
expect(count).toBe(3);

for (let i = 0; i < count; i++) {
const card = cards.nth(i);
const h3 = card.locator('h3');
const priceContainer = card.locator('.min-h-\\[3\\.25rem\\]');

const h3Box = await h3.boundingBox();
const priceBox = await priceContainer.boundingBox();

if (h3Box) {
expect(h3Box.height).toBeGreaterThanOrEqual(40);
}
if (priceBox) {
expect(priceBox.height).toBeGreaterThanOrEqual(52);
}
}
});
});
214 changes: 180 additions & 34 deletions e2e/routes/app/novelty-grid-visual.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,25 @@ test.describe('Novelty Grid Advanced Visual & A11y @mobile', () => {
test.beforeEach(async ({ context }) => {
await context.addInitScript(() => {
const defaultFlags = {
mfa: 'false',
ai_recommendations: 'true',
presentation_mode: 'true',
voice_commands: 'true',
magic_up: 'true',
e2e_tests: 'true',
advanced_analytics: 'true',
custom_kits_v2: 'false',
'mfa': 'false',
'ai_recommendations': 'true',
'presentation_mode': 'true',
'voice_commands': 'true',
'magic_up': 'true',
'e2e_tests': 'true',
'advanced_analytics': 'true',
'custom_kits_v2': 'false'
};

Object.entries(defaultFlags).forEach(([flag, value]) => {
localStorage.setItem(`ff_${flag}`, value);
});

if ('serviceWorker' in navigator) {
navigator.serviceWorker.getRegistrations().then((regs) => {
for (const r of regs) r.unregister();
navigator.serviceWorker.getRegistrations().then(registrations => {
for (const registration of registrations) {
registration.unregister();
}
});
}
});
Expand All @@ -36,58 +40,200 @@ test.describe('Novelty Grid Advanced Visual & A11y @mobile', () => {
test.describe(`Viewport: ${viewport.name}`, () => {
test.use({ viewport: { width: viewport.width, height: viewport.height } });

test('Header immediate rendering', async ({ page }) => {
test('Header Immediate Rendering & Visual', async ({ page }) => {
await page.goto('/novidades', { waitUntil: 'domcontentloaded' });
const title = page.locator('[data-testid="page-title-novidades"]');
const desc = page.locator('[data-testid="novelty-description"]');

const header = page.locator('div.flex.flex-col.gap-4').first();
const title = header.locator('[data-testid="page-title-novidades"]');
const desc = header.locator('[data-testid="novelty-description"]');

await expect(title).toBeVisible();
await expect(title).toHaveText('Novidades');
await expect(desc).toHaveText(
'Produtos recém-chegados ao catálogo nos últimos 30 dias',
);
await expect(desc).toHaveText('Produtos recém-chegados ao catálogo nos últimos 30 dias');

await expect(header).toHaveScreenshot(`novelty-header-only-${viewport.name}.png`);
});

test('Grid responsive columns & scroll alignment', async ({ page }) => {
test('Grid Visual Regression & Scroll', async ({ page }) => {
await page.goto('/novidades');
const grid = page.locator('div[role="list"][aria-label="Grade de novidades"]');
const grid = page.locator('div[role="list"]');
await grid.waitFor({ state: 'visible' });
await page.waitForTimeout(800);
await page.waitForTimeout(1000);

await expect(grid).toHaveScreenshot(`novelty-grid-initial-${viewport.name}.png`, {
maxDiffPixelRatio: 0.02,
});

await grid.evaluate((el) => (el.scrollTop = 1000));
await page.waitForTimeout(600);
await grid.evaluate(el => el.scrollTop = 1000);
await page.waitForTimeout(800);

await expect(grid).toHaveScreenshot(`novelty-grid-scrolled-${viewport.name}.png`, {
maxDiffPixelRatio: 0.02,
});
});

test('Accessibility scan', async ({ page }) => {
test('Accessibility Scan', async ({ page }) => {
await page.goto('/novidades');
const grid = page.locator('div[role="list"][aria-label="Grade de novidades"]');
const grid = page.locator('div[role="list"]');
await grid.waitFor({ state: 'visible' });

const results = await new AxeBuilder({ page })
.include('div[role="list"][aria-label="Grade de novidades"]')
.include('div[role="list"]')
.analyze();
expect(results.violations).toEqual([]);

if (results.violations.length > 0) {
console.error(`A11y Violations for viewport ${viewport.name}:`);
results.violations.forEach(v => {
console.error(`- [${v.id}] ${v.help} (${v.impact})`);
console.error(` URL: ${v.helpUrl}`);
console.error(` Nodes: ${v.nodes.length}`);
});
}

expect(results.violations, `A11y violations found in ${viewport.name}: ${results.violations.map(v => v.id).join(', ')}`).toEqual([]);
});

test('Browser Preferences - Accessibility Consistency', async ({ page }) => {
await page.addStyleTag({
content: `
html { font-size: 20px !important; }
* { transition: none !important; animation: none !important; }
`
});

await page.goto('/novidades');
const grid = page.locator('div[role="list"]');
await grid.waitFor({ state: 'visible' });

await expect(grid).toHaveScreenshot(`novelty-grid-a11y-prefs-${viewport.name}.png`);
});

test('Keyboard Navigation', async ({ page }) => {
await page.goto('/novidades');
await page.keyboard.press('Tab');

const activeElement = await page.evaluate(() => document.activeElement?.tagName);
expect(activeElement).toBeDefined();

await page.keyboard.press('Tab');
await expect(page).toHaveScreenshot(`novelty-keyboard-focus-${viewport.name}.png`);
});
});
}

test('Card alignment edge cases (title + price min-heights)', async ({ page }) => {
test('Skeleton State & Layout Stability', async ({ page }) => {
await page.route('**/api/external-db', async route => {
if (route.request().postDataJSON()?.operation === 'select') {
await new Promise(resolve => setTimeout(resolve, 2000));
}
await route.continue();
});

await page.goto('/novidades');
const skeleton = page.locator('.animate-spin').first();
await expect(skeleton).toBeVisible();

const grid = page.locator('div.grid').filter({ has: page.locator('.animate-pulse') }).first();
await expect(grid).toHaveScreenshot('novelty-skeleton-state.png');

await page.waitForSelector('div[role="list"]');
const realGrid = page.locator('div[role="list"]');
await expect(realGrid).toBeVisible();
await expect(realGrid).toHaveScreenshot('novelty-data-loaded-stability.png');
});

test('Pagination & Alignment Check', async ({ page }) => {
await page.route('**/api/external-db', async route => {
const body = route.request().postDataJSON();
if (body?.operation === 'select' && body?.table === 'products') {
const mockProducts = Array.from({ length: 45 }, (_, i) => ({
id: `page-mock-${i}`,
name: `Product ${i} ${i % 3 === 0 ? 'with a very very very long name to test wrapping and alignment consistency across the grid' : ''}`,
sku: `SKU-${i}`,
primary_image_url: null,
sale_price: i % 5 === 0 ? null : 100 + i,
category_id: 'cat-1',
supplier_id: 'sup-1',
created_at: new Date().toISOString(),
stock_quantity: 100,
min_quantity: 10
}));
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ records: mockProducts, count: 45 })
});
} else {
await route.continue();
}
});

await page.goto('/novidades');
const paginator = page.locator('nav[aria-label="pagination"]');
await paginator.waitFor();

const firstPageItems = page.locator('div[role="listitem"]');
await expect(firstPageItems).toHaveCount(20);
await expect(page).toHaveScreenshot('novelty-pagination-page-1.png');

await page.click('a[aria-label="Go to next page"]');
await page.waitForTimeout(500);
await expect(page.locator('div[role="listitem"]')).toHaveCount(20);
await expect(page).toHaveScreenshot('novelty-pagination-page-2.png');

await page.click('a:text("3")');
await page.waitForTimeout(500);
await expect(page.locator('div[role="listitem"]')).toHaveCount(5);
await expect(page).toHaveScreenshot('novelty-pagination-last-page.png');
});

test('Card Variations: Long Title & Consultation Price', async ({ page }) => {
await page.route('**/api/external-db', async route => {
const body = route.request().postDataJSON();
if (body?.operation === 'select' && body?.table === 'products') {
const mockProducts = [
{
id: 'var-1',
name: 'Short Title',
sku: 'SKU-1',
sale_price: 100,
created_at: new Date().toISOString(),
stock_quantity: 100,
min_quantity: 10
},
{
id: 'var-2',
name: 'This is a very long product name that should definitely wrap to multiple lines and potentially push the layout down if not handled correctly by min-height constraints',
sku: 'SKU-2',
sale_price: 200,
created_at: new Date().toISOString(),
stock_quantity: 100,
min_quantity: 10
},
{
id: 'var-3',
name: 'Consultation Price Item',
sku: 'SKU-3',
sale_price: null,
created_at: new Date().toISOString(),
stock_quantity: 100,
min_quantity: 10
}
];
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ records: mockProducts })
});
} else {
await route.continue();
}
});

await page.goto('/novidades');
const cards = page.locator('div[role="listitem"]');
await cards.first().waitFor();

const allCards = await cards.all();
for (const card of allCards.slice(0, 3)) {
const h3 = card.locator('h3');
const h3Box = await h3.boundingBox();
if (h3Box) expect(h3Box.height).toBeGreaterThanOrEqual(40);
}
await expect(cards).toHaveCount(3);

await expect(page.locator('div[role="list"]')).toHaveScreenshot('novelty-card-variations.png');
});
});
Loading
Loading