mirror of
https://github.com/appwrite/console.git
synced 2026-04-07 19:17:46 +00:00
Merge pull request #1088 from appwrite/tests-e2e-journeys
tests: e2e journeys
This commit is contained in:
@@ -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
|
||||
@@ -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
@@ -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
|
||||
|
||||
Generated
+12
-12
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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-**');
|
||||
});
|
||||
});
|
||||
@@ -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());
|
||||
});
|
||||
@@ -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());
|
||||
});
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user