Skip to content

Commit ca4d866

Browse files
authored
feat: smoke test, e2e config (#472)
* feat: smoke test, e2e config * feat: more tests * fix: deps * fix: timeout * fix: test * fix: tests
1 parent 60a3fa0 commit ca4d866

14 files changed

Lines changed: 467 additions & 292 deletions

File tree

.github/workflows/ci.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ on:
2121
jobs:
2222
build:
2323
runs-on: ubuntu-latest
24-
timeout-minutes: 10
24+
timeout-minutes: 15
2525
permissions:
2626
contents: read
2727

@@ -55,5 +55,11 @@ jobs:
5555
- name: Test
5656
run: pnpm run test
5757

58+
- name: Install Playwright browsers
59+
run: pnpm playwright install --with-deps chromium
60+
61+
- name: E2E tests
62+
run: pnpm run test:e2e
63+
5864
- name: Build
5965
run: pnpm run build

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ vite.config.*.timestamp-*
4242

4343
### Tests ###
4444
/coverage/
45+
/test-results/
46+
/playwright-report/
47+
/blob-report/
4548

4649
### IDE ###
4750
.vscode/*

apps/essence/server/services/channel.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -772,7 +772,7 @@ const links: Channel['links'] = {
772772
value: 'Software para comércio eletrónico',
773773
},
774774
],
775-
to: 'https://nextorders.ru/',
775+
to: 'https://startodel.ru/',
776776
target: '_blank',
777777
},
778778
],

e2e/cart.test.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { expect, test } from '@playwright/test'
2+
3+
test.describe('Cart', () => {
4+
test('add product to cart from product page', async ({ page }) => {
5+
await page.goto('/')
6+
7+
// Click on a product card to navigate to product detail
8+
const productCard = page.locator('main a').filter({ has: page.locator('img') }).first()
9+
await productCard.click()
10+
11+
// Should be on product detail page
12+
await expect(page.locator('h1')).toBeVisible()
13+
14+
// Click "Add to cart" button
15+
const addButton = page.getByRole('button', { name: /add to cart/i })
16+
await expect(addButton).toBeVisible({ timeout: 10_000 })
17+
await addButton.click()
18+
19+
// Cart aside (xl viewport) should now show cart content
20+
const cartTitle = page.locator('aside').getByText('Cart')
21+
await expect(cartTitle).toBeVisible()
22+
})
23+
24+
test('increment and decrement product quantity', async ({ page }) => {
25+
await page.goto('/')
26+
27+
// Navigate to product and add it
28+
const productCard = page.locator('main a').filter({ has: page.locator('img') }).first()
29+
await productCard.click()
30+
await page.getByRole('button', { name: /add to cart/i }).click()
31+
32+
// After adding, +/- buttons should replace "Add to cart"
33+
const plusButton = page.locator('main').getByRole('button', { name: 'Plus' })
34+
const minusButton = page.locator('main').getByRole('button', { name: 'Minus' })
35+
36+
await expect(plusButton).toBeVisible()
37+
await expect(minusButton).toBeVisible()
38+
39+
// Increment
40+
await plusButton.click()
41+
42+
// Decrement twice to remove
43+
await minusButton.click()
44+
await minusButton.click()
45+
46+
// "Add to cart" button should reappear
47+
await expect(page.getByRole('button', { name: /add to cart/i })).toBeVisible()
48+
})
49+
50+
test('cart aside is visible on desktop', async ({ page }) => {
51+
await page.goto('/')
52+
53+
// On xl viewport, aside is always visible
54+
const aside = page.locator('aside')
55+
await expect(aside).toBeVisible()
56+
})
57+
58+
test('navigate to checkout from cart', async ({ page }) => {
59+
await page.goto('/')
60+
61+
// Add a product
62+
const productCard = page.locator('main a').filter({ has: page.locator('img') }).first()
63+
await productCard.click()
64+
await page.getByRole('button', { name: /add to cart/i }).click()
65+
66+
// Click "Okay, next" link in cart aside
67+
const nextButton = page.locator('aside a[href="/checkout"]')
68+
await expect(nextButton).toBeVisible()
69+
await nextButton.click()
70+
71+
await page.waitForURL('**/checkout')
72+
})
73+
74+
test('delivery method tabs are available in cart', async ({ page }) => {
75+
await page.goto('/')
76+
77+
// Add a product
78+
const productCard = page.locator('main a').filter({ has: page.locator('img') }).first()
79+
await productCard.click()
80+
await page.getByRole('button', { name: /add to cart/i }).click()
81+
82+
// Delivery method tabs should be visible in cart aside
83+
const deliveryTab = page.locator('aside').getByRole('tab', { name: /delivery/i })
84+
const pickupTab = page.locator('aside').getByRole('tab', { name: /pickup/i })
85+
86+
await expect(deliveryTab).toBeVisible()
87+
await expect(pickupTab).toBeVisible()
88+
89+
// Switch to pickup
90+
await pickupTab.click()
91+
await expect(pickupTab).toHaveAttribute('aria-selected', 'true')
92+
})
93+
})

e2e/checkout.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { expect, test } from '@playwright/test'
2+
3+
async function addProductToCart(page: import('@playwright/test').Page) {
4+
await page.goto('/')
5+
const productCard = page.locator('main a').filter({ has: page.locator('img') }).first()
6+
await productCard.click()
7+
await page.getByRole('button', { name: /add to cart/i }).click()
8+
// Wait for cart aside to show the item
9+
await expect(page.locator('aside').getByText('Cart')).toBeVisible()
10+
}
11+
12+
test.describe('Checkout', () => {
13+
test('checkout page has contact form', async ({ page }) => {
14+
await addProductToCart(page)
15+
await page.goto('/checkout')
16+
17+
// Contact form with phone input
18+
await expect(page.locator('input[type="tel"]')).toBeVisible()
19+
})
20+
21+
test('submit button is disabled without required fields', async ({ page }) => {
22+
await addProductToCart(page)
23+
await page.goto('/checkout')
24+
25+
// Create order button should be disabled when fields are empty
26+
const submitButton = page.getByRole('button', { name: /create order/i })
27+
await expect(submitButton).toBeDisabled()
28+
})
29+
30+
test('checkout shows info messages', async ({ page }) => {
31+
await addProductToCart(page)
32+
await page.goto('/checkout')
33+
34+
// Should show some info/warning about filling fields
35+
const infoMessage = page.locator('main').getByText(/fill in|required|fields/i)
36+
await expect(infoMessage).toBeVisible()
37+
})
38+
})

e2e/localization.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { expect, test } from '@playwright/test'
2+
3+
test.describe('Localization', () => {
4+
test('content renders in Russian locale', async ({ browser }) => {
5+
const context = await browser.newContext({ locale: 'ru-RU' })
6+
const page = await context.newPage()
7+
8+
await page.goto('/')
9+
10+
await expect(page.locator('html')).toHaveAttribute('lang', 'ru')
11+
await expect(page).toHaveTitle(/Москв/)
12+
13+
await context.close()
14+
})
15+
16+
test('content renders in English locale', async ({ browser }) => {
17+
const context = await browser.newContext({ locale: 'en-US' })
18+
const page = await context.newPage()
19+
20+
await page.goto('/')
21+
22+
await expect(page.locator('html')).toHaveAttribute('lang', 'en')
23+
await expect(page).toHaveTitle(/Moscow/)
24+
25+
await context.close()
26+
})
27+
28+
test('content renders in Spanish locale', async ({ browser }) => {
29+
const context = await browser.newContext({ locale: 'es-ES' })
30+
const page = await context.newPage()
31+
32+
await page.goto('/')
33+
34+
await expect(page.locator('html')).toHaveAttribute('lang', 'es')
35+
36+
await context.close()
37+
})
38+
39+
test('switching locale changes content', async ({ browser }) => {
40+
const context = await browser.newContext({ locale: 'ru-RU' })
41+
const page = await context.newPage()
42+
43+
await page.goto('/')
44+
const ruTitle = await page.title()
45+
46+
await context.close()
47+
48+
const context2 = await browser.newContext({ locale: 'en-US' })
49+
const page2 = await context2.newPage()
50+
51+
await page2.goto('/')
52+
const enTitle = await page2.title()
53+
54+
await context2.close()
55+
56+
// Titles should differ between locales
57+
expect(ruTitle).not.toBe(enTitle)
58+
})
59+
})

e2e/navigation.test.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { expect, test } from '@playwright/test'
2+
3+
test.describe('Navigation', () => {
4+
test('sidebar category link navigates to category page', async ({ page }) => {
5+
await page.goto('/')
6+
7+
const categoryLink = page.locator('nav a[href^="/"]').first()
8+
const href = await categoryLink.getAttribute('href')
9+
10+
await categoryLink.click()
11+
12+
if (href) {
13+
await page.waitForURL(`**${href}`)
14+
}
15+
await expect(page.locator('h1')).toBeVisible()
16+
})
17+
18+
test('category page has content', async ({ page }) => {
19+
await page.goto('/')
20+
21+
const categoryLink = page.locator('nav a[href^="/"]').first()
22+
await categoryLink.click()
23+
24+
// Category page should have heading and some content
25+
await expect(page.locator('h1')).toBeVisible()
26+
await expect(page.locator('main')).toBeVisible()
27+
})
28+
29+
test('product detail page has title and add button', async ({ page }) => {
30+
await page.goto('/')
31+
32+
const productLink = page.locator('main a').filter({ has: page.locator('img') }).first()
33+
await productLink.click()
34+
35+
await expect(page.locator('h1')).toBeVisible()
36+
await expect(page.getByRole('button', { name: /add to cart/i })).toBeVisible()
37+
})
38+
39+
test('mobile menu opens and closes', async ({ browser }) => {
40+
const context = await browser.newContext({
41+
viewport: { width: 375, height: 812 },
42+
})
43+
const page = await context.newPage()
44+
await page.goto('/')
45+
46+
// Wait for Nuxt hydration before interacting
47+
await page.waitForLoadState('networkidle')
48+
49+
// Menu button is visible on mobile (hidden at lg+)
50+
const menuButton = page.locator('header button').first()
51+
await expect(menuButton).toBeVisible()
52+
await menuButton.click()
53+
54+
// Slideover dialog should appear
55+
const dialog = page.getByRole('dialog')
56+
await expect(dialog).toBeVisible({ timeout: 5000 })
57+
58+
// Close via Escape
59+
await page.keyboard.press('Escape')
60+
61+
// Dialog should disappear
62+
await expect(dialog).not.toBeVisible({ timeout: 5000 })
63+
64+
await context.close()
65+
})
66+
67+
test('breadcrumb on product page', async ({ page }) => {
68+
await page.goto('/')
69+
70+
const productLink = page.locator('main a').filter({ has: page.locator('img') }).first()
71+
await productLink.click()
72+
73+
// Breadcrumbs should contain home link
74+
const homeLink = page.getByRole('link', { name: /home/i })
75+
await expect(homeLink).toBeVisible()
76+
})
77+
})

e2e/playwright.config.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import process from 'node:process'
2+
import { defineConfig, devices } from '@playwright/test'
3+
4+
const CI = !!process.env.CI
5+
const ESSENCE_PORT = 3603
6+
const WEB_APP_PORT = 3602
7+
8+
export default defineConfig({
9+
testDir: '.',
10+
fullyParallel: true,
11+
forbidOnly: CI,
12+
retries: CI ? 2 : 0,
13+
workers: CI ? 1 : undefined,
14+
reporter: 'html',
15+
use: {
16+
baseURL: `http://localhost:${WEB_APP_PORT}`,
17+
trace: 'on-first-retry',
18+
},
19+
projects: [
20+
{
21+
name: 'chromium',
22+
use: { ...devices['Desktop Chrome'] },
23+
},
24+
],
25+
webServer: [
26+
{
27+
command: `pnpm nuxt dev --port ${ESSENCE_PORT} --cwd apps/essence`,
28+
port: ESSENCE_PORT,
29+
timeout: 120_000,
30+
reuseExistingServer: !CI,
31+
cwd: '..',
32+
},
33+
{
34+
command: `pnpm nuxt dev --port ${WEB_APP_PORT} --cwd apps/web-app`,
35+
port: WEB_APP_PORT,
36+
timeout: 120_000,
37+
reuseExistingServer: !CI,
38+
cwd: '..',
39+
env: {
40+
NUXT_API_URL: `http://localhost:${ESSENCE_PORT}/api/storefront/gateway`,
41+
NUXT_API_TOKEN: 'test-token',
42+
NUXT_PUBLIC_CHANNEL_ID: 'moscow',
43+
},
44+
},
45+
],
46+
})

0 commit comments

Comments
 (0)