From 60cb3e2c73eee17920374019a6ea4d33f53c0827 Mon Sep 17 00:00:00 2001 From: nbe Date: Wed, 28 Jun 2023 21:43:55 +0300 Subject: [PATCH 1/5] feat: introduce logical direction --- src/lib/components/ToastWrapper.svelte | 2 +- src/lib/core/types.ts | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/lib/components/ToastWrapper.svelte b/src/lib/components/ToastWrapper.svelte index 804eb45..406f1d7 100644 --- a/src/lib/components/ToastWrapper.svelte +++ b/src/lib/components/ToastWrapper.svelte @@ -19,7 +19,7 @@ $: factor = toast.position?.includes('top') ? 1 : -1; $: justifyContent = (toast.position?.includes('center') && 'center') || - (toast.position?.includes('right') && 'flex-end') || + ((toast.position?.includes('right') || toast.position?.includes('end')) && 'flex-end') || null; diff --git a/src/lib/core/types.ts b/src/lib/core/types.ts index eb1c98a..448c5d4 100644 --- a/src/lib/core/types.ts +++ b/src/lib/core/types.ts @@ -1,13 +1,24 @@ import type { SvelteComponent } from 'svelte'; export type ToastType = 'success' | 'error' | 'loading' | 'blank' | 'custom'; +/** Specifies the toast's position on the screen + * + * Logical positions (`start`, `end`) are recommended over absolute positions + * (`left`, `right`), as they automatically adjust based on the text direction + * of the locale (LTR or RTL). Examples: + * - Use `top-start` instead of `top-left`. + * - Use `top-end` instead of `top-right`. */ export type ToastPosition = | 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' - | 'bottom-right'; + | 'bottom-right' + | 'top-start' + | 'top-end' + | 'bottom-start' + | 'bottom-end'; export type Renderable = typeof SvelteComponent | string | null; From 70dccd5bdc0e02b220905ae48244a204d05e4bac Mon Sep 17 00:00:00 2001 From: nbe Date: Thu, 29 Jun 2023 01:20:06 +0300 Subject: [PATCH 2/5] test: init component tests --- .gitignore | 3 + package.json | 3 +- playwright.config.ts | 30 +++++-- pnpm-lock.yaml | 98 ++++++++++++++++++--- tests/components/boilerplate/App.svelte | 14 +++ tests/components/boilerplate/index.html | 6 ++ tests/components/boilerplate/index.ts | 2 + tests/components/boilerplate/types.ts | 5 ++ tests/components/direction.spec.ts | 112 ++++++++++++++++++++++++ tests/components/toast.spec.ts | 11 +++ 10 files changed, 264 insertions(+), 20 deletions(-) create mode 100644 tests/components/boilerplate/App.svelte create mode 100644 tests/components/boilerplate/index.html create mode 100644 tests/components/boilerplate/index.ts create mode 100644 tests/components/boilerplate/types.ts create mode 100644 tests/components/direction.spec.ts create mode 100644 tests/components/toast.spec.ts diff --git a/.gitignore b/.gitignore index ac7211b..3bb86b6 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,6 @@ node_modules !.env.example vite.config.js.timestamp-* vite.config.ts.timestamp-* +.cache +test-results +playwright-report diff --git a/package.json b/package.json index a114f26..73e3bd8 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,8 @@ "svelte": "^3.57.0 || ^4.0.0" }, "devDependencies": { - "@playwright/test": "^1.31.2", + "@playwright/experimental-ct-svelte": "^1.35.1", + "@playwright/test": "^1.35.1", "@sveltejs/adapter-auto": "^2.0.0", "@sveltejs/kit": "^1.20.5", "@sveltejs/package": "^2.1.0", diff --git a/playwright.config.ts b/playwright.config.ts index 80731be..6c8f4aa 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,11 +1,23 @@ -import type { PlaywrightTestConfig } from '@playwright/test'; +import { defineConfig, devices } from '@playwright/experimental-ct-svelte'; -const config: PlaywrightTestConfig = { - webServer: { - command: 'npm run build && npm run preview', - port: 4173 +export default defineConfig({ + testDir: './tests/components', + timeout: 10 * 1000, + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: process.env.CI ? [['github'], ['html']] : [['html', { open: 'never' }]], + use: { + trace: 'retain-on-failure', + video: 'retain-on-failure', + ctPort: 3100, + ctTemplateDir: './tests/components/boilerplate' }, - testDir: 'tests' -}; - -export default config; + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] } + } + ] +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 023fd70..187c9c3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: '6.1' +lockfileVersion: '6.0' settings: autoInstallPeers: true @@ -10,9 +10,12 @@ dependencies: version: 3.1.0(svelte@4.0.0) devDependencies: + '@playwright/experimental-ct-svelte': + specifier: ^1.35.1 + version: 1.35.1(svelte@4.0.0)(vite@4.2.0) '@playwright/test': - specifier: ^1.31.2 - version: 1.31.2 + specifier: ^1.35.1 + version: 1.35.1 '@sveltejs/adapter-auto': specifier: ^2.0.0 version: 2.0.0(@sveltejs/kit@1.20.5) @@ -396,13 +399,48 @@ packages: fastq: 1.15.0 dev: true - /@playwright/test@1.31.2: - resolution: {integrity: sha512-BYVutxDI4JeZKV1+ups6dt5WiqKhjBtIYowyZIJ3kBDmJgsuPKsqqKNIMFbUePLSCmp2cZu+BDL427RcNKTRYw==} - engines: {node: '>=14'} + /@playwright/experimental-ct-core@1.35.1: + resolution: {integrity: sha512-NSoUf6JDLeZFy0HiENwA1GkIwZHvg5KrygnZknwWs7O8yksYLsmiuMb09sf2zsZmfYgVen401SNgf3KfekbweA==} + engines: {node: '>=16'} + hasBin: true + dependencies: + '@playwright/test': 1.35.1 + vite: 4.3.9 + transitivePeerDependencies: + - '@types/node' + - less + - sass + - stylus + - sugarss + - terser + dev: true + + /@playwright/experimental-ct-svelte@1.35.1(svelte@4.0.0)(vite@4.2.0): + resolution: {integrity: sha512-7CV6pXyZMX9IQl0+J9+eNIv1hZj23z9hMA0GHZr7EubkbJvneIB2HsMKgEGxgW/KSGRqPMBNResfl2ShmHgHHQ==} + engines: {node: '>=16'} + hasBin: true + dependencies: + '@playwright/experimental-ct-core': 1.35.1 + '@sveltejs/vite-plugin-svelte': 2.4.2(svelte@4.0.0)(vite@4.2.0) + transitivePeerDependencies: + - '@types/node' + - less + - sass + - stylus + - sugarss + - supports-color + - svelte + - terser + - vite + dev: true + + /@playwright/test@1.35.1: + resolution: {integrity: sha512-b5YoFe6J9exsMYg0pQAobNDR85T1nLumUYgUTtKm4d21iX2L7WqKq9dW8NGJ+2vX0etZd+Y7UeuqsxDXm9+5ZA==} + engines: {node: '>=16'} hasBin: true dependencies: '@types/node': 18.15.3 - playwright-core: 1.31.2 + playwright-core: 1.35.1 optionalDependencies: fsevents: 2.3.2 dev: true @@ -1896,9 +1934,9 @@ packages: pathe: 1.1.0 dev: true - /playwright-core@1.31.2: - resolution: {integrity: sha512-a1dFgCNQw4vCsG7bnojZjDnPewZcw7tZUNFN0ZkcLYKj+mPmXvg4MpaaKZ5SgqPsOmqIf2YsVRkgqiRDxD+fDQ==} - engines: {node: '>=14'} + /playwright-core@1.35.1: + resolution: {integrity: sha512-pNXb6CQ7OqmGDRspEjlxE49w+4YtR6a3X6mT1hZXeJHWmsEz7SunmvZeiG/+y1yyMZdHnnn73WKYdtV1er0Xyg==} + engines: {node: '>=16'} hasBin: true dev: true @@ -2120,6 +2158,14 @@ packages: fsevents: 2.3.2 dev: true + /rollup@3.25.3: + resolution: {integrity: sha512-ZT279hx8gszBj9uy5FfhoG4bZx8c+0A1sbqtr7Q3KNWIizpTdDEPZbV2xcbvHsnFp4MavCQYZyzApJ+virB8Yw==} + engines: {node: '>=14.18.0', npm: '>=8.0.0'} + hasBin: true + optionalDependencies: + fsevents: 2.3.2 + dev: true + /run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} dependencies: @@ -2622,6 +2668,38 @@ packages: fsevents: 2.3.2 dev: true + /vite@4.3.9: + resolution: {integrity: sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==} + engines: {node: ^14.18.0 || >=16.0.0} + hasBin: true + peerDependencies: + '@types/node': '>= 14' + less: '*' + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + esbuild: 0.17.12 + postcss: 8.4.24 + rollup: 3.25.3 + optionalDependencies: + fsevents: 2.3.2 + dev: true + /vitefu@0.2.4(vite@4.2.0): resolution: {integrity: sha512-fanAXjSaf9xXtOOeno8wZXIhgia+CZury481LsDaV++lSvcU2R9Ch2bPh3PYFyoHW+w9LqAeYRISVQjUIew14g==} peerDependencies: diff --git a/tests/components/boilerplate/App.svelte b/tests/components/boilerplate/App.svelte new file mode 100644 index 0000000..8254fcd --- /dev/null +++ b/tests/components/boilerplate/App.svelte @@ -0,0 +1,14 @@ + + + diff --git a/tests/components/boilerplate/index.html b/tests/components/boilerplate/index.html new file mode 100644 index 0000000..74896d0 --- /dev/null +++ b/tests/components/boilerplate/index.html @@ -0,0 +1,6 @@ + + +
+ + + diff --git a/tests/components/boilerplate/index.ts b/tests/components/boilerplate/index.ts new file mode 100644 index 0000000..cee3b6f --- /dev/null +++ b/tests/components/boilerplate/index.ts @@ -0,0 +1,2 @@ +// Apply theme here, add anything your component needs at runtime here. +// import '../src/common.css'; diff --git a/tests/components/boilerplate/types.ts b/tests/components/boilerplate/types.ts new file mode 100644 index 0000000..df02ce6 --- /dev/null +++ b/tests/components/boilerplate/types.ts @@ -0,0 +1,5 @@ +import type toast from '$lib'; +import type { test } from '@playwright/experimental-ct-svelte'; + +export type Mount = Parameters[1]>['0']['mount']; +export type ToastProps = Parameters; diff --git a/tests/components/direction.spec.ts b/tests/components/direction.spec.ts new file mode 100644 index 0000000..274170e --- /dev/null +++ b/tests/components/direction.spec.ts @@ -0,0 +1,112 @@ +import { expect, test } from '@playwright/experimental-ct-svelte'; +import App from './boilerplate/App.svelte'; + +test.use({ viewport: { width: 500, height: 500 } }); + +test.describe('LTR', () => { + test.beforeEach(async ({ page }) => { + await page.evaluate(() => { + document.documentElement.lang = 'en'; + document.documentElement.dir = 'ltr'; + }); + }); + + test('top-start', async ({ mount }) => { + const component = await mount(App, { + props: { toastProps: ['Hello world!', { position: 'top-start' }] } + }); + + const toast = component.getByRole('status'); + // toast should be top left + const toastRect = await toast.boundingBox(); + expect(toastRect?.x).toBeLessThan(150); + }); + + test('top-end', async ({ mount }) => { + const component = await mount(App, { + props: { toastProps: ['Hello world!', { position: 'top-end' }] } + }); + + const toast = component.getByRole('status'); + // toast should be top right + const toastRect = await toast.boundingBox(); + expect(toastRect?.x).toBeGreaterThan(350); + }); + + test('bottom-start', async ({ mount }) => { + const component = await mount(App, { + props: { + toastProps: ['Hello world!', { position: 'bottom-start' }] + } + }); + + const toast = component.getByRole('status'); + // toast should be bottom left + const toastRect = await toast.boundingBox(); + expect(toastRect?.x).toBeLessThan(150); + }); + + test('bottom-end', async ({ mount }) => { + const component = await mount(App, { + props: { toastProps: ['Hello world!', { position: 'bottom-end' }] } + }); + + const toast = component.getByRole('status'); + // toast should be bottom right + const toastRect = await toast.boundingBox(); + expect(toastRect?.x).toBeGreaterThan(350); + }); +}); + +test.describe('RTL', () => { + test.beforeEach(async ({ page }) => { + await page.evaluate(() => { + document.documentElement.lang = 'ar'; + document.documentElement.dir = 'rtl'; + }); + }); + + test('top-start', async ({ mount }) => { + const component = await mount(App, { + props: { toastProps: ['مرحبا', { position: 'top-start' }] } + }); + + const toast = component.getByRole('status'); + // toast should be top right + const toastRect = await toast.boundingBox(); + expect(toastRect?.x).toBeGreaterThan(350); + }); + + test('top-end', async ({ mount }) => { + const component = await mount(App, { + props: { toastProps: ['مرحبا', { position: 'top-end' }] } + }); + + const toast = component.getByRole('status'); + // toast should be top left + const toastRect = await toast.boundingBox(); + expect(toastRect?.x).toBeLessThan(150); + }); + + test('bottom-start', async ({ mount }) => { + const component = await mount(App, { + props: { toastProps: ['مرحبا', { position: 'bottom-start' }] } + }); + + const toast = component.getByRole('status'); + // toast should be bottom right + const toastRect = await toast.boundingBox(); + expect(toastRect?.x).toBeGreaterThan(350); + }); + + test('bottom-end', async ({ mount }) => { + const component = await mount(App, { + props: { toastProps: ['مرحبا', { position: 'bottom-end' }] } + }); + + const toast = component.getByRole('status'); + // toast should be bottom left + const toastRect = await toast.boundingBox(); + expect(toastRect?.x).toBeLessThan(150); + }); +}); diff --git a/tests/components/toast.spec.ts b/tests/components/toast.spec.ts new file mode 100644 index 0000000..36c3c85 --- /dev/null +++ b/tests/components/toast.spec.ts @@ -0,0 +1,11 @@ +import { expect, test } from '@playwright/experimental-ct-svelte'; +import App from './boilerplate/App.svelte'; + +test.use({ viewport: { width: 500, height: 500 } }); + +test('should render', async ({ mount }) => { + const component = await mount(App, { + props: { toastProps: ['Hello world!'] } + }); + await expect(component.getByText('Hello world!')).toBeVisible(); +}); From 98c5a29a86699edb1ad33830591aac07caf7079f Mon Sep 17 00:00:00 2001 From: nbe Date: Thu, 29 Jun 2023 03:50:53 +0300 Subject: [PATCH 3/5] ci: create test workflow --- .github/workflows/ci.yml | 27 +++++++++++++++++++++++++++ playwright.config.ts | 8 ++++++++ 2 files changed, 35 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5e09579 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +name: Playwright Tests +on: + push: + pull_request: +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: pnpm/action-setup@v2 + with: + version: 8 + - uses: actions/setup-node@v3 + with: + node-version: 18 + - name: Install dependencies + run: pnpm install + - name: Install Playwright Browsers + run: pnpm exec playwright install --with-deps + - name: Run Playwright tests + run: pnpm run test + - uses: actions/upload-artifact@v3 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/playwright.config.ts b/playwright.config.ts index 6c8f4aa..a67e38a 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -18,6 +18,14 @@ export default defineConfig({ { name: 'chromium', use: { ...devices['Desktop Chrome'] } + }, + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] } + }, + { + name: 'webkit', + use: { ...devices['Desktop Safari'] } } ] }); From 30df6f4724fff58881fc673d7f10641ed359faea Mon Sep 17 00:00:00 2001 From: nbe Date: Thu, 29 Jun 2023 05:34:05 +0300 Subject: [PATCH 4/5] ci: use pre-built playwright image to speed up CI --- .github/workflows/ci.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5e09579..6de0c58 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,10 @@ on: jobs: test: runs-on: ubuntu-latest + # Use a container to significantly speed up the workflow. The downside is + # that the playwright version needs to be manually kept in sync. + container: + image: mcr.microsoft.com/playwright:v1.35.1 steps: - uses: actions/checkout@v3 - uses: pnpm/action-setup@v2 @@ -15,10 +19,9 @@ jobs: node-version: 18 - name: Install dependencies run: pnpm install - - name: Install Playwright Browsers - run: pnpm exec playwright install --with-deps - name: Run Playwright tests - run: pnpm run test + # BUG: firefox tests fail in container. To fix, we manually set HOME variable: https://github.com/microsoft/playwright/issues/6500#issuecomment-1577284385 + run: env HOME=/root pnpm run test - uses: actions/upload-artifact@v3 if: always() with: From 87a0334ee49244be04b20e5169ab6e6ec48989eb Mon Sep 17 00:00:00 2001 From: nbe Date: Thu, 29 Jun 2023 09:46:59 +0300 Subject: [PATCH 5/5] remove unused type --- tests/components/boilerplate/types.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/components/boilerplate/types.ts b/tests/components/boilerplate/types.ts index df02ce6..c6bb817 100644 --- a/tests/components/boilerplate/types.ts +++ b/tests/components/boilerplate/types.ts @@ -1,5 +1,3 @@ import type toast from '$lib'; -import type { test } from '@playwright/experimental-ct-svelte'; -export type Mount = Parameters[1]>['0']['mount']; export type ToastProps = Parameters;