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)'],
+ }
});