Skip to content

Commit

Permalink
feat(examples): Adding E2E coverage for website intro demos (#1201)
Browse files Browse the repository at this point in the history
Adds playwright e2e suite for testing the intro demos, similar to the
website e2e's but the duplication is necessary due to 1) the submodule
approach and 2) the different build settings and deployments (the
website tests e2e agains the actual deployed netlify build)
  • Loading branch information
msfstef committed Apr 25, 2024
1 parent 784f737 commit f7aa5d9
Show file tree
Hide file tree
Showing 10 changed files with 2,854 additions and 1,092 deletions.
34 changes: 33 additions & 1 deletion .github/workflows/examples_e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ concurrency:
cancel-in-progress: true

jobs:
test_web:
test_web_examples:
timeout-minutes: 30
runs-on: ubuntu-latest
strategy:
Expand Down Expand Up @@ -48,3 +48,35 @@ jobs:
run: |
pnpm install --frozen-lockfile &&
pnpm web-e2e
test_website_demos:
timeout-minutes: 30
runs-on: ubuntu-latest
strategy:
fail-fast: false
defaults:
run:
working-directory: 'examples/introduction'
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with:
version: 8
- uses: actions/setup-node@v4
with:
node-version: 18
cache: pnpm
- name: Install dependencies
run: yarn
- name: Start backend, run migrations, generate client
run: |
yarn backend:up &&
yarn db:migrate &&
yarn client:generate
- name: Start dev server in background
run: yarn dev < /dev/null &
- name: Run e2e tests
working-directory: 'examples/_testing'
run: |
pnpm install --frozen-lockfile &&
pnpm website-demos-e2e
202 changes: 202 additions & 0 deletions examples/_testing/e2e/website-demos/demos.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import { test, expect } from '@playwright/test'
import type { Page, Locator } from '@playwright/test'

const INITIAL_SYNC_TIME_MS = 4000
const EXPECTED_SQLITE_TIME_MS = 1000
const EXPECTED_SYNC_TIME_MS = 3000
const CONNECTIVITY_TIME_MS = 200

function expectSqlite(cb: () => number | Promise<number>) {
return expect.poll(cb, { timeout: EXPECTED_SQLITE_TIME_MS })
}

function expectSync(cb: () => number | Promise<number>) {
return expect.poll(cb, { timeout: EXPECTED_SYNC_TIME_MS })
}

async function prepareDemo(page: Page, demo: Locator) {
await expect(demo).toBeAttached({ timeout: 3000 })
// wait for initial sync to occur
await page.waitForTimeout(INITIAL_SYNC_TIME_MS)
await demo.scrollIntoViewIfNeeded()
}

test.describe('website demos', () => {
test.beforeEach(async ({ page }) => {
page.goto('/')
})

test('local-first instant demo', async ({ page }) => {
const demo = page.getByTestId('local-first-instant-demo')
await prepareDemo(page, demo)

const demoLocal = demo.getByTestId('local-first')
const localItems = demoLocal.getByTestId('item')
const localLatencyTxt = demoLocal.getByText('Latency')
const localAddBtn = demoLocal.getByText('Add')
const localClearBtn = demoLocal.getByText('Clear')

// check local operations work
const initLocalItems = await localItems.count()
await localAddBtn.click()
await expectSqlite(() => localItems.count()).toBe(initLocalItems + 1)

// keep track of latency
const localLatencyText = await localLatencyTxt.textContent()
await expect(localLatencyText).toMatch(/Latency: \d+ms/)
const localLatency = Number(localLatencyText?.match(/\d+/)![0])

await localClearBtn.click()
await expectSqlite(() => localItems.count()).toBe(0)

// should have a latency smaller than 150ms
await expect(localLatency).toBeLessThan(150)
})

test('multi-user realtime demo', async ({ page }) => {
const demo = page.getByTestId('multiuser-realtime-demo')
await prepareDemo(page, demo)

const user1Items = demo.getByTestId('user1').getByTestId('item')
const user1AddBtn = demo.getByTestId('user1').getByText('Add')
const user1ClearBtn = demo.getByTestId('user1').getByText('Clear')

const user2Items = demo.getByTestId('user2').getByTestId('item')
const user2AddBtn = demo.getByTestId('user2').getByText('Add')

// user 1 and 2 should match in number of items
const initNumItems = await user1Items.count()
await expect(await user2Items.count()).toBe(initNumItems)

// user 1 adds an item, should see response _almost_ immediately
// and should eventually sync to user 2
await user1AddBtn.click()
await expectSqlite(() => user1Items.count()).toBe(initNumItems + 1)
await expectSync(() => user2Items.count()).toBe(initNumItems + 1)

// should work in reverse as well
await user2AddBtn.click()
await expectSqlite(() => user2Items.count()).toBe(initNumItems + 2)
await expectSync(() => user1Items.count()).toBe(initNumItems + 2)

// clearing should also sync across users
await user1ClearBtn.click()
await expectSqlite(() => user1Items.count()).toBe(0)
await expectSync(() => user1Items.count()).toBe(0)
})

test('offline connectivity demo', async ({ page }) => {
const demo = page.getByTestId('offline-connectivity-demo')
await prepareDemo(page, demo)

const user1Items = demo.getByTestId('user1').getByTestId('item')
const user1ConnectivityToggle = demo
.getByTestId('user1')
.getByTestId('connectivity-toggle')
const user1AddBtn = demo.getByTestId('user1').getByText('Add')

const user2Items = demo.getByTestId('user2').getByTestId('item')
const user2ConnectivityToggle = demo
.getByTestId('user2')
.getByTestId('connectivity-toggle')
const user2ClearBtn = demo.getByTestId('user2').getByText('Clear')

// user 1 and 2 should match in number of items
const initNumItems = await user1Items.count()
await expect(await user2Items.count()).toBe(initNumItems)

// go offline for user 1
await user1ConnectivityToggle.click()
await expect(
demo.getByTestId('user1').getByText('Disconnected')
).toBeVisible()
await page.waitForTimeout(CONNECTIVITY_TIME_MS)

// user 1 adds items, should see changes _almost_ immediately
// but user 2 should not see them as user 1 is offline
await user1AddBtn.click()
await expectSqlite(() => user1Items.count()).toBe(initNumItems + 1)
await user1AddBtn.click()
await expectSqlite(() => user1Items.count()).toBe(initNumItems + 2)
await page.waitForTimeout(EXPECTED_SYNC_TIME_MS)
await expect(await user2Items.count()).toBe(initNumItems)

// go online for user 1
await user1ConnectivityToggle.click()
await expect(demo.getByTestId('user1').getByText('Connected')).toBeVisible()

// should eventually see change reflected on user 2
await expectSync(async () => await user2Items.count()).toBe(
initNumItems + 2
)

await user2ConnectivityToggle.click()
await expect(
demo.getByTestId('user2').getByText('Disconnected')
).toBeVisible()
await page.waitForTimeout(CONNECTIVITY_TIME_MS)
await user2ClearBtn.click()

// should work in reverse as well - clear offline user 2
await user2ClearBtn.click()
await expectSqlite(() => user2Items.count()).toBe(0)
await page.waitForTimeout(EXPECTED_SYNC_TIME_MS)
await expect(await user1Items.count()).toBe(initNumItems + 2)

// connect user 2 back again
await user2ConnectivityToggle.click()
await expect(demo.getByTestId('user2').getByText('Connected')).toBeVisible()

// user 1 items should eventually be cleared
await expectSync(() => user1Items.count()).toBe(0)
})

test('offline integrity demo', async ({ page }) => {
const demo = page.getByTestId('offline-integrity-demo')
await prepareDemo(page, demo)

const user1Player = demo.getByTestId('user1').getByTestId('player').first()
const user1Tournaments = demo.getByTestId('user1').getByTestId('tournament')
const user1ConnectivityToggle = demo
.getByTestId('user1')
.getByTestId('connectivity-toggle')

const user2Tournaments = demo.getByTestId('user2').getByTestId('tournament')
const user2ConnectivityToggle = demo
.getByTestId('user2')
.getByTestId('connectivity-toggle')

// go offline for both users
await user1ConnectivityToggle.click()
await expect(
demo.getByTestId('user1').getByText('Disconnected')
).toBeVisible()
await user2ConnectivityToggle.click()
await expect(
demo.getByTestId('user2').getByText('Disconnected')
).toBeVisible()
await page.waitForTimeout(CONNECTIVITY_TIME_MS)

// drag a player into the tournement for user 1
await user1Player.dragTo(await user1Tournaments.first())

// delete all tournamnets for user 2
const numTournaments = await user2Tournaments.count()
for (let i = 0; i < numTournaments; i++) {
await user2Tournaments.first().locator('svg').click()
await page.waitForTimeout(EXPECTED_SQLITE_TIME_MS)
}

await expect(await user2Tournaments.count()).toBe(0)

// go online for both users
await user1ConnectivityToggle.click()
await expect(demo.getByTestId('user1').getByText('Connected')).toBeVisible()
await user2ConnectivityToggle.click()
await expect(demo.getByTestId('user2').getByText('Connected')).toBeVisible()

// both users should have 1 tournament
await expectSync(() => user1Tournaments.count()).toBe(1)
await expectSync(() => user2Tournaments.count()).toBe(1)
})
})
12 changes: 7 additions & 5 deletions examples/_testing/package.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
{
"name": "electric-sql-example-tests",
"name": "@internal/electric-sql-example-tests",
"version": "0.1.0",
"author": "ElectricSQL",
"license": "Apache-2.0",
"type": "module",
"scripts": {
"web-e2e": "npx playwright install --with-deps && npx playwright test"
"e2e-base": "playwright install --with-deps && playwright test",
"website-demos-e2e": "npm run e2e-base ./e2e/website-demos/*.spec.ts",
"web-e2e": "npm run e2e-base ./e2e/web/*.spec.ts"
},
"devDependencies": {
"@playwright/test": "^1.43.1",
"@types/node": "^20.12.7",
"@typescript-eslint/eslint-plugin": "^7.7.0",
"@typescript-eslint/parser": "^7.7.0",
"eslint": "^9.0.0",
"@typescript-eslint/eslint-plugin": "^7.7.1",
"@typescript-eslint/parser": "^7.7.1",
"eslint": "^8.56.0",
"typescript": "^5.4.5"
}
}
8 changes: 1 addition & 7 deletions examples/_testing/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
import { defineConfig, devices } from '@playwright/test'

/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();

/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './e2e/web',
testDir: './e2e',
/* Run tests in files in parallel */
fullyParallel: false,
/* Fail the build on CI if you accidentally left test.only in the source code. */
Expand Down
4 changes: 2 additions & 2 deletions examples/_testing/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
"noFallthroughCasesInSwitch": true,
},
"include": ["e2e/**/*.ts, playwright.config.ts"]
"include": ["e2e/**/*.ts", "playwright.config.ts"]
}
2 changes: 1 addition & 1 deletion examples/introduction/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ yarn db:migrate
yarn client:generate
```

If running outside of the context of the website repo, you will need to populate some environment variables in `src/config.ts`. These are normally injected by Webpack during the website build but this source repo uses Vite as its build process.
If running outside of the context of the website repo, you will need to ensure the configuration replacement variables in`vite.config.ts`'s `define` object are set correctly.

Boot up the dev server

Expand Down
11 changes: 0 additions & 11 deletions examples/introduction/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,6 @@
// Commented out dummy values - the regular ones are injected by Webpack
// for the website build but the source repo uses Vite as its build tool
// const __BACKEND_URL__ = 'http://localhost:40001'
// const __DEBUG_MODE__ = true
// const __ELECTRIC_URL__ = 'ws://localhost:5133'
// const __SANITISED_DATABASE_URL__ = 'dummy'

// @ts-expect-error - injected by webpack
export const BACKEND_URL: string = __BACKEND_URL__
// @ts-expect-error - injected by webpack
export const SANITISED_DATABASE_URL: string = __SANITISED_DATABASE_URL__
// @ts-expect-error - injected by webpack
export const DEBUG_MODE: boolean = __DEBUG_MODE__
// @ts-expect-error - injected by webpack
export const ELECTRIC_URL: string = __ELECTRIC_URL__

// Verify that the database URL does not contain credentials.
Expand Down
4 changes: 4 additions & 0 deletions examples/introduction/src/env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
declare const __BACKEND_URL__: string
declare const __SANITISED_DATABASE_URL__: string
declare const __DEBUG_MODE__: boolean
declare const __ELECTRIC_URL__: string
6 changes: 6 additions & 0 deletions examples/introduction/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
envPrefix: 'ELECTRIC_',
define: {
__BACKEND_URL__: JSON.stringify('http://localhost:40001'),
__DEBUG_MODE__: true,
__ELECTRIC_URL__: JSON.stringify('ws://localhost:5133'),
__SANITISED_DATABASE_URL__: JSON.stringify('dummy'),
},
optimizeDeps: {
exclude: ['wa-sqlite'],
},
Expand Down
Loading

0 comments on commit f7aa5d9

Please sign in to comment.