Merge pull request #1088 from appwrite/tests-e2e-journeys

tests: e2e journeys
This commit is contained in:
Torsten Dittmann
2024-05-15 15:29:45 +02:00
committed by GitHub
15 changed files with 250 additions and 81 deletions
+33
View File
@@ -0,0 +1,33 @@
name: E2E
on:
pull_request:
branches: ['**']
paths-ignore:
- '**/*.md'
- 'static/**/*'
env:
VITE_STRIPE_PUBLIC_KEY: ${{ vars.VITE_STRIPE_PUBLIC_KEY }}
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: 20
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps chromium
- name: E2E Tests
run: npm run e2e
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 7
+1 -1
View File
@@ -28,6 +28,6 @@ jobs:
- name: Linter
run: npm run lint
- name: Unit Tests
run: npm test
run: npm run test
- name: Build Console
run: npm run build
+3 -1
View File
@@ -8,6 +8,8 @@ node_modules
.env
.env.*
!.env.example
test-results/
playwright-report/
.DS_STORE
.cache
@@ -145,4 +147,4 @@ dist
.stylelintcache
# SvelteKit build / generate output
.svelte-kit
.svelte-kit
+12 -12
View File
@@ -29,7 +29,7 @@
"devDependencies": {
"@melt-ui/pp": "^0.1.4",
"@melt-ui/svelte": "^0.61.2",
"@playwright/test": "^1.37.1",
"@playwright/test": "^1.44.0",
"@sveltejs/adapter-static": "^3.0.1",
"@sveltejs/kit": "^2.3.4",
"@sveltejs/vite-plugin-svelte": "^3.0.1",
@@ -1895,12 +1895,12 @@
}
},
"node_modules/@playwright/test": {
"version": "1.43.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.43.1.tgz",
"integrity": "sha512-HgtQzFgNEEo4TE22K/X7sYTYNqEMMTZmFS8kTq6m8hXj+m1D8TgwgIbumHddJa9h4yl4GkKb8/bgAl2+g7eDgA==",
"version": "1.44.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.0.tgz",
"integrity": "sha512-rNX5lbNidamSUorBhB4XZ9SQTjAqfe5M+p37Z8ic0jPFBMo5iCtQz1kRWkEMg+rYOKSlVycpQmpqjSFq7LXOfg==",
"dev": true,
"dependencies": {
"playwright": "1.43.1"
"playwright": "1.44.0"
},
"bin": {
"playwright": "cli.js"
@@ -7678,12 +7678,12 @@
}
},
"node_modules/playwright": {
"version": "1.43.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.43.1.tgz",
"integrity": "sha512-V7SoH0ai2kNt1Md9E3Gwas5B9m8KR2GVvwZnAI6Pg0m3sh7UvgiYhRrhsziCmqMJNouPckiOhk8T+9bSAK0VIA==",
"version": "1.44.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.0.tgz",
"integrity": "sha512-F9b3GUCLQ3Nffrfb6dunPOkE5Mh68tR7zN32L4jCk4FjQamgesGay7/dAAe1WaMEGV04DkdJfcJzjoCKygUaRQ==",
"dev": true,
"dependencies": {
"playwright-core": "1.43.1"
"playwright-core": "1.44.0"
},
"bin": {
"playwright": "cli.js"
@@ -7696,9 +7696,9 @@
}
},
"node_modules/playwright-core": {
"version": "1.43.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.43.1.tgz",
"integrity": "sha512-EI36Mto2Vrx6VF7rm708qSnesVQKbxEWvPrfA1IPY6HgczBplDx7ENtx+K2n4kJ41sLLkuGfmb0ZLSSXlDhqPg==",
"version": "1.44.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.0.tgz",
"integrity": "sha512-ZTbkNpFfYcGWohvTTl+xewITm7EOuqIqex0c7dNZ+aXsbrLj0qI8XlGKfPpipjm0Wny/4Lt4CJsWJk1stVS5qQ==",
"dev": true,
"bin": {
"playwright-core": "cli.js"
+3 -2
View File
@@ -15,7 +15,8 @@
"test": "TZ=EST vitest run",
"test:ui": "TZ=EST vitest --ui",
"test:watch": "TZ=EST vitest watch",
"e2e": "playwright test tests/e2e"
"e2e": "playwright test tests/e2e",
"e2e:ui": "playwright test tests/e2e --ui"
},
"dependencies": {
"@appwrite.io/console": "^0.6.1",
@@ -41,7 +42,7 @@
"devDependencies": {
"@melt-ui/pp": "^0.1.4",
"@melt-ui/svelte": "^0.61.2",
"@playwright/test": "^1.37.1",
"@playwright/test": "^1.44.0",
"@sveltejs/adapter-static": "^3.0.1",
"@sveltejs/kit": "^2.3.4",
"@sveltejs/vite-plugin-svelte": "^3.0.1",
+8
View File
@@ -1,7 +1,15 @@
import { type PlaywrightTestConfig } from '@playwright/test';
const config: PlaywrightTestConfig = {
timeout: 120000,
reportSlowTests: null,
reporter: [['html', { open: 'never' }]],
webServer: {
timeout: 120000,
env: {
VITE_APPWRITE_ENDPOINT: 'http://console-tests.appwrite.org/v1',
VITE_CONSOLE_MODE: 'cloud'
},
command: 'npm run build && npm run preview',
port: 4173
}
+21
View File
@@ -0,0 +1,21 @@
export function getOrganizationIdFromUrl(pathname: string) {
const regex = /\/console\/organization-([^/]+)(\/.*)?/;
const match = pathname.match(regex);
if (match) {
return match[1];
}
throw new Error('Organization ID not found in pathname');
}
export function getProjectIdFromUrl(pathname: string) {
const regex = /\/console\/project-([^/]+)(\/.*)?/;
const match = pathname.match(regex);
if (match) {
return match[1];
}
throw new Error('Project ID not found in pathname');
}
@@ -0,0 +1,8 @@
import { test } from '@playwright/test';
import { registerUserStep } from '../steps/account';
import { createFreeProject } from '../steps/free-project';
test('onboarding - free tier', async ({ page }) => {
await registerUserStep(page);
await createFreeProject(page);
});
@@ -0,0 +1,8 @@
import { test } from '@playwright/test';
import { registerUserStep } from '../steps/account';
import { createProProject } from '../steps/pro-project';
test('onboarding - pro', async ({ page }) => {
await registerUserStep(page);
await createProProject(page);
});
@@ -0,0 +1,20 @@
import { test } from '@playwright/test';
import { registerUserStep } from '../steps/account';
import { createFreeProject } from '../steps/free-project';
import { enterAddress, enterCreditCard } from '../steps/pro-project';
test('upgrade - free tier', async ({ page }) => {
await registerUserStep(page);
await createFreeProject(page);
await test.step('upgrade project', async () => {
await page.getByRole('button', { name: 'upgrade' }).click();
await page.locator('input[value="tier-1"]').click();
await page.getByRole('button', { name: 'next' }).click();
await enterCreditCard(page);
await enterAddress(page);
// skip members
await page.getByRole('button', { name: 'next' }).click();
await page.getByRole('button', { name: 'create' }).click();
await page.waitForURL('/console/organization-**');
});
});
-54
View File
@@ -1,54 +0,0 @@
import { expect, test } from '@playwright/test';
/*TODO: Things to test in login:
- presence of forgot password link
- validation message on wrong input
- correct response and redirect after login
- logout works
- back button does not log out user
- forward button does not log in user after logout
- limit to total number of login attempts
-
*/
test('login page has inputs and button', async ({ page }) => {
await page.goto('/login');
const mail = page.locator('id=email');
const pass = page.locator('id=password');
const button = page.locator('button:has-text("Sign in")');
expect(await mail.isVisible());
expect(await pass.isVisible());
expect(await button.isVisible());
});
test('login page has a working sign up link', async ({ page }) => {
await page.goto('/login');
await page.waitForTimeout(100);
const signup = page.locator('a[href="/register"]');
expect(await signup.isVisible());
});
test('login page inputs are navigable by keyboard', async ({ page }) => {
await page.goto('/login');
const mail = page.locator('id=email');
await mail.focus();
await page.keyboard.type('wrongemail@apppwrite.io');
await page.keyboard.press('Tab');
await page.keyboard.type('password');
await page.keyboard.press('Tab');
await page.keyboard.press('Enter');
expect(await page.locator('.toaster-item').isVisible());
expect(await page.locator('text=Invalid credentials').isVisible());
});
test('login page shows error & response is 401 with wrong inputs', async ({ page }) => {
await page.goto('/login');
await page.fill('id=email', 'wrongemail@apppwrite.io');
await page.fill('id=password', 'wrongpassword');
await page.click('button:has-text("Sign in")');
page.on('response', (response) => {
expect(response.status()).toBe(401);
});
expect(await page.locator('.toaster-item').isVisible());
expect(await page.locator('text=Invalid credentials').isVisible());
});
-11
View File
@@ -1,11 +0,0 @@
import { expect, test } from '@playwright/test';
test('register page has inputs', async ({ page }) => {
await page.goto('/register');
const name = page.locator('id=name');
const mail = page.locator('id=email');
const pass = page.locator('id=password');
expect(await name.isVisible());
expect(await mail.isVisible());
expect(await pass.isVisible());
});
+34
View File
@@ -0,0 +1,34 @@
import { test, type Page } from '@playwright/test';
type Metadata = {
name: string;
email: string;
password: string;
};
export function registerUserStep(page: Page): Promise<Metadata> {
return test.step('register user', async () => {
const seed = crypto.randomUUID();
await page.goto('/register');
await page.getByRole('button', { name: 'only required' }).click();
const inputs = {
name: page.locator('id=name'),
email: page.locator('id=email'),
password: page.locator('id=password'),
terms: page.locator('id=terms')
};
const values = {
name: 'testuser ' + seed,
email: 'testuser+' + seed + '@apppwrite.io',
password: 'testuser+' + seed + '@apppwrite.io'
};
await inputs.name.fill(values.name);
await inputs.email.fill(values.email);
await inputs.password.fill(values.password);
await inputs.terms.check();
await page.getByRole('button', { name: 'Sign up', exact: true }).click();
await page.waitForURL('/console/onboarding');
return values;
});
}
+37
View File
@@ -0,0 +1,37 @@
import { test, expect, type Page } from '@playwright/test';
import { getOrganizationIdFromUrl, getProjectIdFromUrl } from '../helpers/url';
type Metadata = {
id: string;
organizationId: string;
};
export async function createFreeProject(page: Page): Promise<Metadata> {
const organizationId = await test.step('create organization', async () => {
await page.goto('/console');
await page.waitForURL('/console/onboarding');
await page.locator('id=name').fill('test org');
await page.locator('id=plan').selectOption('tier-0');
await page.getByRole('button', { name: 'get started' }).click();
await page.waitForURL('/console/organization-**');
return getOrganizationIdFromUrl(page.url());
});
const projectId = await test.step('create project', async () => {
await page.waitForURL('/console/organization-**');
await page.getByRole('button', { name: 'create project' }).first().click();
await page.locator('id=name').fill('test project');
await page.getByRole('button', { name: 'next' }).click();
await page.locator('label').filter({ hasText: 'Frankfurt' }).click();
await page.getByRole('button', { name: 'create' }).click();
await page.waitForURL('/console/project-**/overview/platforms');
expect(page.url()).toContain('/console/project-');
return getProjectIdFromUrl(page.url());
});
return {
id: projectId,
organizationId
};
}
+62
View File
@@ -0,0 +1,62 @@
import { test, expect, type Page } from '@playwright/test';
import { getOrganizationIdFromUrl, getProjectIdFromUrl } from '../helpers/url';
type Metadata = {
id: string;
organizationId: string;
};
export async function enterCreditCard(page: Page) {
await page.getByPlaceholder('cardholder').fill('Test User');
const stripe = page.frameLocator('[title="Secure payment input frame"]');
await stripe.locator('id=Field-numberInput').fill('4242424242424242');
await stripe.locator('id=Field-expiryInput').fill('1250');
await stripe.locator('id=Field-cvcInput').fill('123');
await stripe.locator('id=Field-countryInput').selectOption('DE');
await page.getByRole('button', { name: 'Next' }).click();
}
export async function enterAddress(page: Page) {
await page.locator('id=country').selectOption('US');
await page.locator('id=address').fill('123 Test St');
await page.locator('id=city').fill('Test City');
await page.locator('id=state').fill('Test State');
await page.getByRole('button', { name: 'Next' }).click();
}
export async function createProProject(page: Page): Promise<Metadata> {
const organizationId = await test.step('create organization', async () => {
await page.goto('/console');
await page.waitForURL('/console/onboarding');
await page.locator('id=name').fill('test org');
await page.locator('id=plan').selectOption('tier-1');
await page.getByRole('button', { name: 'get started' }).click();
await enterCreditCard(page);
await enterAddress(page);
// skip members
await page.getByRole('button', { name: 'next' }).click();
// start pro trial
await page.getByRole('button', { name: 'create' }).click();
await page.waitForURL('/console/organization-**');
return getOrganizationIdFromUrl(page.url());
});
const projectId = await test.step('create project', async () => {
await page.waitForURL('/console/organization-**');
await page.getByRole('button', { name: 'create project' }).first().click();
await page.getByPlaceholder('project name').fill('test project');
await page.getByRole('button', { name: 'next' }).click();
await page.locator('label').filter({ hasText: 'frankfurt' }).click();
await page.getByRole('button', { name: 'create' }).click();
await page.waitForURL('/console/project-**/overview/platforms');
expect(page.url()).toContain('/console/project-');
return getProjectIdFromUrl(page.url());
});
return {
id: projectId,
organizationId
};
}