Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
7 changes: 6 additions & 1 deletion packages/app-client/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,9 @@ gitignore
.DS_Store
Thumbs.db

cache
cache

/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();
});
6 changes: 3 additions & 3 deletions packages/app-client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

<link rel="preload" href="/api/config" as="fetch" crossorigin="anonymous" />

<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.">

<link rel="author" href="humans.txt" />
Expand All @@ -17,14 +17,14 @@
<!-- 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
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 @@ -44,7 +45,9 @@
"devDependencies": {
"@antfu/eslint-config": "^3.0.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.10.0",
"jsdom": "^25.0.0",
"typescript": "^5.3.3",
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 @@ -4,7 +4,7 @@ import { TextField } from '@/modules/ui/components/textfield';
import { type Component, createSignal } from 'solid-js';
import { createRandomPassword } from '../notes.models';

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 { t } = useI18n();

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

return (
<div class="border border-input rounded-md flex items-center pr-1">
<TextField placeholder={t('create.settings.password.placeholder')} 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={t('create.settings.password.placeholder')} 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 @@ -137,16 +137,21 @@ export const CreateNotePage: Component = () => {
<Switch>
<Match when={!getIsNoteCreated()}>
<TextFieldRoot class="w-full ">
<TextArea placeholder={t('create.settings.placeholder')} class="flex-1 p-4 min-h-300px sm:min-h-700px" value={getContent()} onInput={e => updateContent(e.currentTarget.value)} />
<TextArea
placeholder={t('create.settings.placeholder')}
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>
{t('create.settings.password.label')}
</TextFieldLabel>
<NotePasswordField getPassword={getPassword} setPassword={setPassword} />

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

<TextFieldRoot class="w-full">
Expand Down Expand Up @@ -190,7 +195,7 @@ export const CreateNotePage: Component = () => {
{t('create.settings.delete-after-reading.label')}
</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 @@ -200,7 +205,7 @@ export const CreateNotePage: Component = () => {
</TextFieldRoot>

<div>
<FileUploaderButton variant="secondary" class="mt-2 w-full" multiple onFilesUpload={({ files }) => setUploadedFiles(prevFiles => [...prevFiles, ...files])}>
<FileUploaderButton variant="secondary" class="mt-2 w-full" multiple onFilesUpload={({ files }) => setUploadedFiles(prevFiles => [...prevFiles, ...files])} data-test-id="create-note">
<div class="i-tabler-upload mr-2 text-lg text-muted-foreground"></div>
{t('create.settings.attach-files')}
</FileUploaderButton>
Expand Down Expand Up @@ -258,7 +263,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 @@ -41,10 +41,10 @@ const RequestPasswordForm: Component<{ onPasswordEntered: (args: { password: str
<div>
<TextFieldRoot>
<TextFieldLabel>{t('view.request-password.form.label')}</TextFieldLabel>
<TextField type="password" placeholder={t('view.request-password.form.placeholder')} value={getPassword()} onInput={e => updatePassword(e.currentTarget.value)} autofocus />
<TextField type="password" placeholder={t('view.request-password.form.placeholder')} 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">
<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>
{t('view.request-password.form.unlock-button')}
</Button>
Expand Down Expand Up @@ -262,7 +262,7 @@ export const ViewNotePage: Component = () => {
{getDecryptedNote() && (
<div class="flex-1 mb-4">
<div class="flex items-center gap-2 mb-4 justify-between">
<div class="text-muted-foreground">
<div class="text-muted-foreground" data-test-id="note-content-display">
{t('view.note-content')}
</div>
<CopyButton text={getDecryptedNote()!} variant="secondary" />
Expand Down
4 changes: 4 additions & 0 deletions packages/app-client/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import process from 'node:process';
import unoCssPlugin from 'unocss/vite';
import { defineConfig } from 'vite';
import solidPlugin from 'vite-plugin-solid';
import { configDefaults } from 'vitest/config';

export default defineConfig({
plugins: [
Expand All @@ -29,4 +30,7 @@ export default defineConfig({
'@': path.resolve(__dirname, './src'),
},
},
test: {
exclude: [...configDefaults.exclude, '**/*.e2e.test.ts'],
},
});
Loading
Loading