diff --git a/.gitignore b/.gitignore index 9906ba0..66c2b40 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,7 @@ vite.config.js.timestamp-* vite.config.ts.timestamp-* .idea/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/README.md b/README.md index a1b38b9..51869e4 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Powered by [`create-svelte`]( https://github.com/sveltejs/kit/tree/main/packages ## Developing ```bash +npm npm install npm run dev @@ -37,7 +38,15 @@ the repo: ```bash make start # starts the frequency node -Then run the tests: +``` +Then if you don't have Playwright installed, install it: +```bash +npx playwright install --with-deps +``` + +Finally, run tests. +```bash npm run test +npx playwright test ``` diff --git a/package-lock.json b/package-lock.json index fcc0f85..36f7d08 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ }, "devDependencies": { "@eslint/js": "^9.9.0", + "@playwright/test": "^1.48.2", "@polkadot/types-codec": "^12.4.1", "@sveltejs/adapter-static": "^3.0.4", "@sveltejs/kit": "2.5.24", @@ -30,6 +31,7 @@ "@testing-library/user-event": "^14.5.2", "@types/eslint__js": "^8.42.3", "@types/jest": "^29.5.12", + "@types/node": "^22.8.4", "@typescript-eslint/eslint-plugin": "^8.2.0", "@typescript-eslint/parser": "^8.2.0", "@vitest/coverage-v8": "^2.0.5", @@ -1868,6 +1870,21 @@ "node": ">=14" } }, + "node_modules/@playwright/test": { + "version": "1.48.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.2.tgz", + "integrity": "sha512-54w1xCWfXuax7dz4W2M9uw0gDyh+ti/0K/MxcCUxChFh37kkdxPdfZDw5QBbuPUJHr1CiHJ1hXgSs+GgeQc5Zw==", + "dev": true, + "dependencies": { + "playwright": "1.48.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.25", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.25.tgz", @@ -3227,11 +3244,11 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.14.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.2.tgz", - "integrity": "sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==", + "version": "22.8.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.4.tgz", + "integrity": "sha512-SpNNxkftTJOPk0oN+y2bIqurEXHTA2AOZ3EJDDKeJ5VzkvvORSvmQXGQarcOzWV1ac7DCaPBEdMDxBsM+d8jWw==", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.8" } }, "node_modules/@types/pug": { @@ -7820,6 +7837,50 @@ "node": ">=8" } }, + "node_modules/playwright": { + "version": "1.48.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.2.tgz", + "integrity": "sha512-NjYvYgp4BPmiwfe31j4gHLa3J7bD2WiBz8Lk2RoSsmX38SVIARZ18VYjxLjAcDsAhA+F4iSEXTSGgjua0rrlgQ==", + "dev": true, + "dependencies": { + "playwright-core": "1.48.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.48.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.2.tgz", + "integrity": "sha512-sjjw+qrLFlriJo64du+EK0kJgZzoQPsabGF4lBvsid+3CNIZIYLgnMj9V6JY5VhM2Peh20DJWIVpVljLLnlawA==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/postcss": { "version": "8.4.47", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", @@ -9374,9 +9435,9 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" }, "node_modules/universalify": { "version": "0.2.0", diff --git a/package.json b/package.json index 1b7aa61..90f38ff 100644 --- a/package.json +++ b/package.json @@ -8,10 +8,10 @@ "preview": "vite preview", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", - "test": "vitest", - "test:e2e": "vitest run --dir ./test/e2e", + "test": "vitest run && npx playwright test", + "test:e2e": "vitest run ./test/e2e", "test:stake": "vitest stake", - "test:ci": "vitest run --dir ./test/unit-and-integration --coverage", + "test:ci": "vitest run ./test/unit-and-integration --coverage", "test:ui": "vitest --ui", "test:watch": "vitest src", "coverage": "vitest run --coverage", @@ -23,6 +23,7 @@ }, "devDependencies": { "@eslint/js": "^9.9.0", + "@playwright/test": "^1.48.2", "@polkadot/types-codec": "^12.4.1", "@sveltejs/adapter-static": "^3.0.4", "@sveltejs/kit": "2.5.24", @@ -33,6 +34,7 @@ "@testing-library/user-event": "^14.5.2", "@types/eslint__js": "^8.42.3", "@types/jest": "^29.5.12", + "@types/node": "^22.8.4", "@typescript-eslint/eslint-plugin": "^8.2.0", "@typescript-eslint/parser": "^8.2.0", "@vitest/coverage-v8": "^2.0.5", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..3c4b721 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,80 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// import path from 'path'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './test/playwright', + testMatch: /.*.test.ts/, + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://127.0.0.1:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + // currently failing: + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + + /* Test against mobile viewports. */ + { + name: 'Mobile Chrome', + use: { ...devices['Pixel 5'] }, + }, + { + name: 'Mobile Safari', + use: { ...devices['iPhone 12'] }, + }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: 'npm run build && npm run preview', + port: 4173, + reuseExistingServer: !process.env.CI, + }, +}); diff --git a/src/components/BackHomeButton.svelte b/src/components/BackHomeButton.svelte new file mode 100644 index 0000000..d5abfbe --- /dev/null +++ b/src/components/BackHomeButton.svelte @@ -0,0 +1,5 @@ + + +{cancelText} diff --git a/src/components/BecomeAProvider.svelte b/src/components/BecomeAProvider.svelte index b65ce71..becb2f8 100644 --- a/src/components/BecomeAProvider.svelte +++ b/src/components/BecomeAProvider.svelte @@ -6,13 +6,8 @@ import CreateMsa from './CreateMsa.svelte'; import CreateProvider from './CreateProvider.svelte'; import EmailProviderRequest from './EmailProviderRequest.svelte'; - import { pageContent } from '$lib/stores/pageContentStore'; import { NetworkType } from '$lib/stores/networksStore'; - - // a callback for when the user cancels this action - export let cancelAction = () => { - pageContent.login(); - }; + import BackHomeButton from '$components/BackHomeButton.svelte';
@@ -34,7 +29,7 @@ {/if} {:else} - + {/if} diff --git a/src/components/CreateMsa.svelte b/src/components/CreateMsa.svelte index a99c767..4ec1ca8 100644 --- a/src/components/CreateMsa.svelte +++ b/src/components/CreateMsa.svelte @@ -10,11 +10,7 @@ import LoadingIcon from '$lib/assets/LoadingIcon.svelte'; import ActivityLogPreviewItem from './ActivityLogPreviewItem.svelte'; import { activityLog } from '$lib/stores/activityLogStore'; - - // a callback for when the user cancels this action - export let cancelAction = () => { - pageContent.login(); - }; + import BackToRootButton from '$components/BackHomeButton.svelte'; let recentActivityItem: Activity | undefined; let recentTxnId: Activity['txnId'] | undefined; @@ -66,7 +62,7 @@ Create an MSA {/if} - +
{#if recentActivityItem} diff --git a/src/components/CreateProvider.svelte b/src/components/CreateProvider.svelte index 704d840..37442fa 100644 --- a/src/components/CreateProvider.svelte +++ b/src/components/CreateProvider.svelte @@ -9,11 +9,8 @@ import LoadingIcon from '$lib/assets/LoadingIcon.svelte'; import { activityLog } from '$lib/stores/activityLogStore'; import ActivityLogPreviewItem from './ActivityLogPreviewItem.svelte'; + import BackHomeButton from '$components/BackHomeButton.svelte'; - // a callback for when the user cancels this action - export let cancelAction = () => { - pageContent.login(); - }; // a callback for when a transaction hits a final state let createProviderTxnFinished = async (succeeded: boolean) => { if (succeeded) { @@ -82,7 +79,7 @@ Create Provider {/if} - + {#if recentActivityItem} diff --git a/src/components/EmailProviderRequest.svelte b/src/components/EmailProviderRequest.svelte index 0d40fc6..dd10653 100644 --- a/src/components/EmailProviderRequest.svelte +++ b/src/components/EmailProviderRequest.svelte @@ -1,13 +1,9 @@ {#if $pageContent === PageContent.Dashboard} -{:else if $pageContent === PageContent.Login} +{:else} -{:else if $pageContent === PageContent.BecomeProvider} - {/if} diff --git a/src/routes/become-a-provider/+page.svelte b/src/routes/become-a-provider/+page.svelte new file mode 100644 index 0000000..7ce8f17 --- /dev/null +++ b/src/routes/become-a-provider/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/test/e2e/page.test.ts b/test/e2e/page.test.ts index aed3173..503453b 100644 --- a/test/e2e/page.test.ts +++ b/test/e2e/page.test.ts @@ -9,7 +9,7 @@ globalThis.alert = () => {}; const getByTextContent = (text) => { // Passing function to `getByText` - return screen.getByText((content, element) => { + return screen.getByText((_content, element) => { const hasText = (element) => element.textContent === text; const elementHasText = hasText(element); const childrenDontHaveText = Array.from(element?.children || []).every((child) => !hasText(child)); @@ -26,14 +26,18 @@ describe('displays correct component', () => { it('renders ProviderLogin component when $pageContent is PageContent.Login', async () => { pageContent.login(); - const { container } = render(Page); - expect(container.querySelector('#provider-login') as HTMLElement).toBeInTheDocument(); + const { getByText, getByTestId } = render(Page); + expect(getByText(/Provider Login/)).toBeInTheDocument(); + expect(getByText(/Not a Provider\?/)).toBeInTheDocument(); + expect(getByTestId('become-a-provider')).toBeInTheDocument(); }); +}); - it('renders BecomeAProvider component when $pageContent is PageContent.BecomeProvider', async () => { - pageContent.becomeProvider(); - const { container } = render(Page); - expect(container.querySelector('#become-a-provider') as HTMLElement).toBeInTheDocument(); +describe('/become-a-provider', () => { + it('is accessible from main page', () => { + pageContent.login(); + const { getByTestId } = render(Page); + expect(getByTestId('become-a-provider')).toBeInTheDocument(); }); }); diff --git a/test/playwright/navigation.test.ts b/test/playwright/navigation.test.ts new file mode 100644 index 0000000..06166fd --- /dev/null +++ b/test/playwright/navigation.test.ts @@ -0,0 +1,25 @@ +import { test, expect } from '@playwright/test'; +let root = 'http://localhost:4173/'; + +test('Navigation from home page', async ({ page }) => { + await page.goto('/'); + await expect(page.getByText('Provider Login')).toBeVisible(); + + // Go to 'Become a Provider' + await page.getByTestId('become-a-provider').click(); + await page.waitForURL('**/become-a-provider'); + expect(page.getByText(/Become a Provider/)).toBeVisible(); + + await page.getByText(/Back/).click(); + await page.waitForURL('**/'); + + // Go to FAQ + await page.getByText(/FAQ/).click(); + await page.waitForURL('**/faq'); + expect(page.getByText(/What is the difference between Mainnet and Testnets\?/)).toBeVisible(); + + // Click the Home button + await page.getByText(/Home/).click(); + await page.waitForURL('**/'); + await expect(page.getByText('Provider Login')).toBeVisible(); +}); diff --git a/test/unit-and-integration/becomeProvider.test.ts b/test/unit-and-integration/becomeProvider.test.ts index 23c893e..52b5d29 100644 --- a/test/unit-and-integration/becomeProvider.test.ts +++ b/test/unit-and-integration/becomeProvider.test.ts @@ -9,17 +9,11 @@ describe('BecomeAProvider component', () => { const mockCancelAction = vi.fn(); it('shows text + Cancel button', () => { - const { container, getByRole } = render(BecomeAProvider, { cancelAction: mockCancelAction }); + const { container, getByTestId } = render(BecomeAProvider); const title = container.querySelector('h2'); expect(title).toHaveTextContent('Become a Provider'); - expect(getByRole('button', { name: 'Back' })).toBeInTheDocument(); - }); - - it('clicking Cancel performs the cancelAction callback', async () => { - const { getByRole } = render(BecomeAProvider, { cancelAction: mockCancelAction }); - - const cancel = getByRole('button', { name: 'Back' }); - fireEvent.click(cancel); - expect(mockCancelAction).toHaveBeenCalled(); + const cancel = getByTestId('back-home'); + expect(cancel).toBeInTheDocument(); + expect(cancel.getAttribute('href')).toEqual('/'); }); }); diff --git a/test/unit-and-integration/createMsa.test.ts b/test/unit-and-integration/createMsa.test.ts index 412ea60..ece1e87 100644 --- a/test/unit-and-integration/createMsa.test.ts +++ b/test/unit-and-integration/createMsa.test.ts @@ -3,6 +3,7 @@ import '@testing-library/jest-dom'; import CreateMsa from '../../src/components/CreateMsa.svelte'; import { vi } from 'vitest'; import { fireEvent, render } from '@testing-library/svelte'; +import { getByTextContent } from '../helpers'; globalThis.alert = () => {}; @@ -13,16 +14,11 @@ describe('CreateMsa component', () => { storeChainInfo.update((val) => (val = { ...val, connected: true })); }); it('shows text + Cancel button', () => { - const { getByRole } = render(CreateMsa, { cancelAction: mockCancelAction }); + const { getByRole, getByText } = render(CreateMsa); expect(getByRole('button', { name: 'Create an MSA' })).toBeInTheDocument(); - expect(getByRole('button', { name: 'Cancel' })).toBeInTheDocument(); - }); - it('clicking Cancel performs the callback', async () => { - const { getByRole } = render(CreateMsa, { cancelAction: mockCancelAction }); - - const cancel = getByRole('button', { name: 'Cancel' }); - fireEvent.click(cancel); - expect(mockCancelAction).toHaveBeenCalled(); + const cancel = getByText('Cancel'); + expect(cancel).toBeInTheDocument(); + expect(cancel.getAttribute('href')).toEqual('/'); }); // TODO: we introduced create api into the parent component, which now breaks the test. diff --git a/test/unit-and-integration/createProvider.test.ts b/test/unit-and-integration/createProvider.test.ts index aa9ea8e..3e8bd14 100644 --- a/test/unit-and-integration/createProvider.test.ts +++ b/test/unit-and-integration/createProvider.test.ts @@ -8,28 +8,20 @@ import userEvent from '@testing-library/user-event'; globalThis.alert = () => {}; describe('CreateProvider component', () => { - const mockCancelAction = vi.fn(); - beforeAll(() => { storeChainInfo.update((val) => (val = { ...val, connected: true })); }); it('shows text + Cancel button', () => { - const { getByRole } = render(CreateProvider, { cancelAction: mockCancelAction }); + const { getByRole, getByText } = render(CreateProvider); expect(getByRole('button', { name: 'Create Provider' })).toBeInTheDocument(); - expect(getByRole('button', { name: 'Cancel' })).toBeInTheDocument(); + const cancel = getByText('Cancel'); + expect(cancel).toBeInTheDocument(); + expect(cancel.getAttribute('href')).toEqual('/'); }); - it('clicking Cancel performs the callback', async () => { - const { getByRole } = render(CreateProvider, { cancelAction: mockCancelAction }); - const cancel = getByRole('button', { name: 'Cancel' }); - fireEvent.click(cancel); - expect(mockCancelAction).toHaveBeenCalled(); - }); it('clicking CreateProvider calls the extrinsic', async () => { userEvent.setup(); - const { getByRole, getByLabelText } = render(CreateProvider, { - cancelAction: mockCancelAction, - }); + const { getByRole, getByLabelText } = render(CreateProvider); let extrinsicWasCalled = false; const mockReady = vi.fn().mockResolvedValue(true); diff --git a/test/unit-and-integration/requestToBeProvider.test.ts b/test/unit-and-integration/requestToBeProvider.test.ts index 972de03..e6bc658 100644 --- a/test/unit-and-integration/requestToBeProvider.test.ts +++ b/test/unit-and-integration/requestToBeProvider.test.ts @@ -11,8 +11,6 @@ import Keyring from '@polkadot/keyring'; globalThis.alert = () => {}; describe('RequestToBeProvider component', () => { - const mockCancelAction = vi.fn(); - beforeEach(() => { storeChainInfo.update((val) => (val = { ...val, connected: true })); @@ -25,26 +23,21 @@ describe('RequestToBeProvider component', () => { }); it('shows text + Cancel button', () => { - const { container, getByRole } = render(RequestToBeProvider, { cancelAction: mockCancelAction }); + const { container, getByRole, getByText } = render(RequestToBeProvider); const title = container.querySelector('h2'); expect(title).toHaveTextContent('Request to Be a Provider'); expect(getByRole('button', { name: 'Submit Request To Be Provider' })).toBeInTheDocument(); - expect(getByRole('button', { name: 'Cancel' })).toBeInTheDocument(); + expect(getByText('Cancel')).toBeInTheDocument(); }); - it('clicking Cancel performs the cancelAction callback', async () => { - const { getByRole } = render(RequestToBeProvider, { cancelAction: mockCancelAction }); - - const cancel = getByRole('button', { name: 'Cancel' }); - fireEvent.click(cancel); - expect(mockCancelAction).toHaveBeenCalled(); + it('clicking Cancel goes back home', async () => { + const { container, getByText } = render(RequestToBeProvider); + const cancel = getByText('Cancel').click(); }); it('clicking Request To Be Provider submits extrinsic and shows Transaction Status', async () => { const user = userEvent.setup(); - const { getByRole, getByLabelText } = render(RequestToBeProvider, { - cancelAction: mockCancelAction, - }); + const { getByRole, getByLabelText } = render(RequestToBeProvider); let extrinsicWasCalled = false; const mockReady = vi.fn().mockResolvedValue(true); diff --git a/vitest.config.ts b/vitest.config.ts index 2951529..c656944 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -22,5 +22,6 @@ export default defineConfig({ coverage: { include: ['src/**'], }, - }, + include: ['test/e2e/*.{test,spec}.?(c|m)[jt]s?(x)', 'test/unit-and-integration/*.{test,spec}.?(c|m)[jt]s?(x)'], + } });