Skip to content

Commit

Permalink
feat(client): added e2e tests
Browse files Browse the repository at this point in the history
  • Loading branch information
CorentinTh committed Aug 28, 2024
1 parent b4277be commit 44299fb
Show file tree
Hide file tree
Showing 12 changed files with 234 additions and 24 deletions.
11 changes: 11 additions & 0 deletions .github/workflows/ci-deploy-cloudflare.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,14 @@ jobs:

- name: Build the app
run: pnpm build

- name: Verify build
run: |
if [ ! -f dist/_worker.js ]; then
echo "dist/_worker.js not found"
exit 1
fi
if [ ! -f dist/index.html ]; then
echo "dist/index.html not found"
exit 1
fi
53 changes: 53 additions & 0 deletions .github/workflows/ci-test-e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: E2E Tests

on:
deployment_status:

jobs:
run-e2es:
if: github.event_name == 'deployment_status' && github.event.deployment_status.state == 'success'
runs-on: ubuntu-latest
timeout-minutes: 60
defaults:
run:
working-directory: packages/app-client

steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
corepack: true
cache: 'pnpm'

- name: Get Playwright version
id: playwright-version
run: echo "PLAYWRIGHT_VERSION=$(jq -r .dependencies.playwright package.json)" >> "$GITHUB_OUTPUT"

- name: Install dependencies
run: pnpm i
working-directory: ./

- name: Restore Playwright browsers from cache
uses: actions/cache@v3
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ steps.playwright-version.outputs.PLAYWRIGHT_VERSION }}-${{ hashFiles('**/playwright.config.ts') }}
restore-keys: |
${{ runner.os }}-playwright-${{ steps.playwright-version.outputs.PLAYWRIGHT_VERSION }}-
${{ runner.os }}-playwright-
- name: Install Playwright Browsers
run: pnpm exec playwright install --with-deps

- name: Run e2e tests
run: pnpm test:e2e
env:
BASE_URL: ${{ github.event.deployment_status.environment_url }}

- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 15
4 changes: 4 additions & 0 deletions packages/app-client/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,7 @@ gitignore
# System Files
.DS_Store
Thumbs.db
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
31 changes: 31 additions & 0 deletions packages/app-client/e2e-tests/createand-view-note.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { expect, test } from '@playwright/test';

test('Can create and view a note', async ({ page }) => {
await page.goto('/');

await expect(page).toHaveTitle('Enclosed - Send private and secure notes');

// Write a note with a password and delete after reading
await page.getByTestId('note-content').fill('Hello, World!');
await page.getByTestId('note-password').fill('my-cat-is-cute');
await page.getByTestId('delete-after-reading').click();

await page.getByTestId('create-note').click();
const noteUrl = await page.getByTestId('note-url').inputValue();

expect(noteUrl).toBeDefined();

await page.goto(noteUrl);

await page.getByTestId('note-password-prompt').fill('my-cat-is-cute');
await page.getByTestId('note-password-submit').click();

const noteContent = await page.getByTestId('note-content-display').textContent();

expect(noteContent).toBe('Hello, World!');

// Refresh the page and check if the note is still there
await page.reload();

await expect(page.getByText('Note not found')).toBeVisible();
});
22 changes: 18 additions & 4 deletions packages/app-client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,20 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<title>Enclosed - Send private and secure notes</title>
<meta name="title" content="Enclosed - Send Private and Secure Notes">
<meta name="title" content="Enclosed - Send private and secure notes">
<meta name="description" content="Enclosed is a secure, end-to-end encrypted platform for sending private notes. Set expiration, passwords, and self-destruct options to keep your notes confidential.">

<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://enclosed.cc/">
<meta property="og:title" content="Enclosed - Send Private and Secure Notes">
<meta property="og:title" content="Enclosed - Send private and secure notes">
<meta property="og:description" content="Enclosed is a secure, end-to-end encrypted platform for sending private notes. Set expiration, passwords, and self-destruct options to keep your notes confidential.">
<meta property="og:image" content="https://enclosed.cc/og-image.png">

<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image">
<meta property="twitter:url" content="https://enclosed.cc/">
<meta property="twitter:title" content="Enclosed - Send Private and Secure Notes">
<meta property="twitter:title" content="Enclosed - Send private and secure notes">
<meta property="twitter:description" content="Enclosed is a secure, end-to-end encrypted platform for sending private notes. Set expiration, passwords, and self-destruct options to keep your notes confidential.">
<meta property="twitter:image" content="https://enclosed.cc/og-image.png">

Expand Down Expand Up @@ -47,9 +47,23 @@
}
}
</script>

<style>
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
</style>
</head>
<body>
<h1 class="sr-only" aria-hidden="true">Enclosed - Send Private and Secure Notes</h1>
<h1 class="sr-only" aria-hidden="true">Enclosed - Send private and secure notes</h1>
<p class="sr-only" aria-hidden="true">Enclosed is a secure platform for sending private notes. All notes are end-to-end encrypted with zero knowledge on the server side.</p>

<noscript>You need to enable JavaScript to run this app.</noscript>
Expand Down
3 changes: 3 additions & 0 deletions packages/app-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"test": "pnpm run test:unit",
"test:unit": "vitest run",
"test:unit:watch": "vitest watch",
"test:e2e": "playwright test",
"typecheck": "tsc --noEmit"
},
"dependencies": {
Expand All @@ -39,7 +40,9 @@
"devDependencies": {
"@antfu/eslint-config": "^2.27.0",
"@iconify-json/tabler": "^1.1.120",
"@playwright/test": "^1.46.1",
"@types/lodash-es": "^4.17.12",
"@types/node": "^22.5.0",
"eslint": "^9.9.0",
"jsdom": "^25.0.0",
"solid-devtools": "^0.30.0",
Expand Down
52 changes: 52 additions & 0 deletions packages/app-client/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import process from 'node:process';
import { defineConfig, devices } from '@playwright/test';

const baseURL = process.env.BASE_URL ?? 'http://localhost:3000/';
const isCI = Boolean(process.env.CI);

/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './e2e-tests',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: isCI,
/* Retry on CI only */
retries: isCI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: isCI ? 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,

/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',

testIdAttribute: 'data-test-id',
locale: 'en-GB',
timezoneId: 'Europe/Paris',
},

/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},

{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},

{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
],
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { TextField } from '@/modules/ui/components/textfield';
import { Button } from '@/modules/ui/components/button';
import { createRandomString } from '@/modules/shared/random/random';

export const NotePasswordField: Component<{ getPassword: () => string; setPassword: (value: string) => void }> = (props) => {
export const NotePasswordField: Component<{ getPassword: () => string; setPassword: (value: string) => void; dataTestId?: string } > = (props) => {
const [getShowPassword, setShowPassword] = createSignal(false);

const generateRandomPassword = () => {
Expand All @@ -15,7 +15,7 @@ export const NotePasswordField: Component<{ getPassword: () => string; setPasswo

return (
<div class="border border-input rounded-md flex items-center pr-1">
<TextField placeholder="Password..." value={props.getPassword()} onInput={e => props.setPassword(e.currentTarget.value)} class="border-none shadow-none focus-visible:ring-none" type={getShowPassword() ? 'text' : 'password'} />
<TextField placeholder="Password..." value={props.getPassword()} onInput={e => props.setPassword(e.currentTarget.value)} class="border-none shadow-none focus-visible:ring-none" type={getShowPassword() ? 'text' : 'password'} data-test-id={props.dataTestId} />

<Button variant="link" onClick={() => setShowPassword(!getShowPassword())} class="text-base size-9 p-0 text-muted-foreground hover:text-primary transition" aria-label={getShowPassword() ? 'Hide password' : 'Show password'}>
<div classList={{ 'i-tabler-eye': !getShowPassword(), 'i-tabler-eye-off': getShowPassword() }}></div>
Expand Down
17 changes: 11 additions & 6 deletions packages/app-client/src/modules/notes/pages/create-note.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,14 +99,19 @@ export const CreateNotePage: Component = () => {
<Switch>
<Match when={!getIsNoteCreated()}>
<TextFieldRoot class="w-full ">
<TextArea placeholder="Type your note here." class="flex-1 p-4 min-h-300px sm:min-h-700px" value={getContent()} onInput={e => updateContent(e.currentTarget.value)} />
<TextArea
placeholder="Type your note here."
class="flex-1 p-4 min-h-300px sm:min-h-700px"
value={getContent()}
onInput={e => updateContent(e.currentTarget.value)}
data-test-id="note-content"
/>
</TextFieldRoot>

<div class="w-full sm:w-320px flex flex-col gap-4 flex-shrink-0">
<TextFieldRoot class="w-full">
<TextFieldLabel>Note password</TextFieldLabel>
<NotePasswordField getPassword={getPassword} setPassword={setPassword} />

<NotePasswordField getPassword={getPassword} setPassword={setPassword} dataTestId="note-password" />
</TextFieldRoot>

<TextFieldRoot class="w-full">
Expand All @@ -128,7 +133,7 @@ export const CreateNotePage: Component = () => {
<TextFieldRoot class="w-full">
<TextFieldLabel>Delete after reading</TextFieldLabel>
<SwitchUiComponent class="flex items-center space-x-2" checked={getDeleteAfterReading()} onChange={setDeleteAfterReading}>
<SwitchControl>
<SwitchControl data-test-id="delete-after-reading">
<SwitchThumb />
</SwitchControl>
<SwitchLabel class="text-sm text-muted-foreground">
Expand All @@ -137,7 +142,7 @@ export const CreateNotePage: Component = () => {
</SwitchUiComponent>
</TextFieldRoot>

<Button class="w-full mt-2" onClick={createNote}>
<Button class="w-full mt-2" onClick={createNote} data-test-id="create-note">
Create note
</Button>

Expand Down Expand Up @@ -169,7 +174,7 @@ export const CreateNotePage: Component = () => {
</div>

<TextFieldRoot class="w-full max-w-800px mt-4">
<TextField value={getNoteUrl()} readonly class="w-full text-center" />
<TextField value={getNoteUrl()} readonly class="w-full text-center" data-test-id="note-url" />
</TextFieldRoot>

<div class="flex items-center gap-2 w-full mx-auto mt-2 justify-center flex-col sm:flex-row">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,13 @@ const RequestPasswordForm: Component<{ onPasswordEntered: (args: { password: str
<div>
<TextFieldRoot>
<TextFieldLabel>Password</TextFieldLabel>
<TextField type="password" placeholder="Note password..." value={getPassword()} onInput={e => updatePassword(e.currentTarget.value)} autofocus />
<TextField type="password" placeholder="Note password..." value={getPassword()} onInput={e => updatePassword(e.currentTarget.value)} autofocus data-test-id="note-password-prompt" />
</TextFieldRoot>
</div>
<Button
class="w-full mt-4"
type="submit"
data-test-id="note-password-submit"
>
<div class="i-tabler-lock-open mr-2 text-lg"></div>
Unlock note
Expand Down Expand Up @@ -177,12 +178,11 @@ export const ViewNotePage: Component = () => {
<div class="mx-auto max-w-1200px px-6 mt-6 flex gap-4 flex-col ">
<Card class="w-full rounded-md shadow-sm">
<CardContent class="p-6">
<div class="whitespace-pre-wrap">{getNoteContent()}</div>
<div class="whitespace-pre-wrap" data-test-id="note-content-display">{getNoteContent()}</div>
</CardContent>
</Card>

<div class="min-w-300px">

<CopyButton text={getNoteContent()} variant="secondary" />
</div>
</div>
Expand Down
10 changes: 4 additions & 6 deletions packages/app-client/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,10 @@ import path from 'node:path';
import { defineConfig } from 'vite';
import solidPlugin from 'vite-plugin-solid';
import unoCssPlugin from 'unocss/vite';
// import devtools from 'solid-devtools/vite';
import { configDefaults } from 'vitest/config';

export default defineConfig({
plugins: [
/*
Uncomment the following line to enable solid-devtools.
For more info see https://github.com/thetarnav/solid-devtools/tree/main/packages/extension#readme
*/
// devtools(),
unoCssPlugin(),
solidPlugin(),
],
Expand All @@ -25,4 +20,7 @@ export default defineConfig({
'@': path.resolve(__dirname, './src'),
},
},
test: {
exclude: [...configDefaults.exclude, '**/*.e2e.test.ts'],
},
});
Loading

0 comments on commit 44299fb

Please sign in to comment.