diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 00000000..b242558a --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,46 @@ +name: End to end Tests +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] +jobs: + test: + timeout-minutes: 10 + defaults: + run: + working-directory: ./tests/end-to-end + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up cargo cache + uses: Swatinem/rust-cache@378c8285a4eaf12899d11bea686a763e906956af + - uses: actions/setup-node@v4 + with: + node-version: lts/* + cache: 'npm' + cache-dependency-path: ./tests/end-to-end/package-lock.json + - name: Install dependencies + run: > + npm ci + npx playwright install --with-deps chromium + - name: build sqlpage + run: cargo build + working-directory: ./examples/official-site + - name: start official site and wait for it to be ready + timeout-minutes: 1 + run: > + cargo run 2>/tmp/stderrlog & + tail -f /tmp/stderrlog | grep -q "started successfully" + working-directory: ./examples/official-site + - name: Run Playwright tests + run: npx playwright test + - name: show server logs + if: failure() + run: cat /tmp/stderrlog + - uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c0e5d4a..93c682d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 0.26.0 +- fix ugly wrapping of items in the header when the page title is long. We now have a nice text ellipsis (...) when the title is too long. - re-add a link to the website title in the shell component - add `text` and `post_html` properties to the [html](https://sql.ophir.dev/documentation.sql?component=html#component) component. This allows to include sanitized user-generated content in the middle of custom HTML. - allow loading javascript ESM modules in the shell component @@ -14,6 +15,7 @@ - fixed a bug where a form input with a value of `0` would diplay as empty instead of showing the `0`. - reduce the margin at the botton of forms to make them more compact. - fix [datagrid](https://sql.ophir.dev/documentation.sql?component=datagrid#component) color pills display when they contain long text. + - fix the "started successfully" message being displayed before the error message when the server failed to start. ## 0.25.0 (2024-07-13) diff --git a/examples/official-site/documentation.sql b/examples/official-site/documentation.sql index c94d9a0d..bc0bd3e3 100644 --- a/examples/official-site/documentation.sql +++ b/examples/official-site/documentation.sql @@ -6,7 +6,10 @@ where $component is not null and not exists (select 1 from component where name -- This line, at the top of the page, tells web browsers to keep the page locally in cache once they have it. select 'http_header' as component, 'public, max-age=600, stale-while-revalidate=3600, stale-if-error=86400' as "Cache-Control"; -select 'dynamic' as component, properties FROM example WHERE component = 'shell' LIMIT 1; +select 'dynamic' as component, json_patch(json_extract(properties, '$[0]'), json_object( + 'title', coalesce($component || ' - ', '') || 'SQLPage Documentation' +)) as properties +FROM example WHERE component = 'shell' LIMIT 1; select 'text' as component, format('SQLPage v%s documentation', sqlpage.version()) as title; select ' diff --git a/examples/official-site/examples/handle_picture_upload.sql b/examples/official-site/examples/handle_picture_upload.sql index e6a38907..66700335 100644 --- a/examples/official-site/examples/handle_picture_upload.sql +++ b/examples/official-site/examples/handle_picture_upload.sql @@ -10,7 +10,7 @@ select 'Your picture' as title, 'Uploaded file type: ' || sqlpage.uploaded_file_mime_type('my_file') as description where $data_url is not null; -select 'form' as component; +select 'form' as component, 'Upload picture' as validate; select 'my_file' as name, 'file' as type, 'Picture' as label; select 'text' as component, ' diff --git a/sqlpage/templates/shell.handlebars b/sqlpage/templates/shell.handlebars index 64a6f25f..cfa4d784 100644 --- a/sqlpage/templates/shell.handlebars +++ b/sqlpage/templates/shell.handlebars @@ -71,7 +71,7 @@ {{#if (or (or title (or icon image)) menu_item)}}
diff --git a/tests/end-to-end/.gitignore b/tests/end-to-end/.gitignore new file mode 100644 index 00000000..68c5d18f --- /dev/null +++ b/tests/end-to-end/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/tests/end-to-end/official-site.spec.ts b/tests/end-to-end/official-site.spec.ts new file mode 100644 index 00000000..9800235a --- /dev/null +++ b/tests/end-to-end/official-site.spec.ts @@ -0,0 +1,60 @@ +import { test, expect } from '@playwright/test'; + +const BASE = 'http://localhost:8080/'; + +test('Open documentation', async ({ page }) => { + await page.goto(BASE); + + // Expect a title "to contain" a substring. + await expect(page).toHaveTitle("SQLPage"); + + // open the submenu + await page.getByText('Documentation', { exact: true }).first().click(); + await page.getByText('All Components').click(); + const components = ['form', 'map', 'chart', 'button']; + for (const component of components) { + await expect(page.getByRole('link', { name: component }).first()).toBeVisible(); + } +}); + +test('chart', async ({ page }) => { + await page.goto(BASE + '/documentation.sql?component=chart#component'); + await expect(page.getByText('Loading...')).not.toBeVisible(); + await expect(page.locator('.apexcharts-canvas').first()).toBeVisible(); +}); + +test('map', async ({ page }) => { + await page.goto(BASE + '/documentation.sql?component=map#component'); + await expect(page.getByText('Loading...')).not.toBeVisible(); + await expect(page.locator('.leaflet-marker-icon').first()).toBeVisible(); +}); + +test('form example', async ({ page }) => { + await page.goto(BASE + '/examples/multistep-form'); + // Single selection matching the value or label + await page.getByLabel('From').selectOption('Paris'); + await page.getByText('Next').click(); + await page.getByLabel(/\bTo\b/).selectOption('Mexico'); + await page.getByText('Next').click(); + await page.getByLabel('Number of Adults').fill('1'); + await page.getByText('Next').click(); + await page.getByLabel('Passenger 1 (adult)').fill('John Doe'); + await page.getByText('Book the flight').click(); + await expect(page.getByText('John Doe').first()).toBeVisible(); +}); + +test('File upload', async ({ page }) => { + await page.goto(BASE); + await page.getByText('Examples', { exact: true }).click(); + await page.getByText('File uploads').click(); + const my_svg = 'Hello World'; + // @ts-ignore + const buffer = Buffer.from(my_svg); + await page.getByLabel('Picture').setInputFiles({ + name: 'small.svg', + mimeType: 'image/svg+xml', + buffer, + }); + await page.getByRole('button', { name: 'Upload picture' }).click(); + await expect(page.locator('img[src^=data]').first().getAttribute('src')).resolves.toBe('data:image/svg+xml;base64,' + buffer.toString('base64')); +}); \ No newline at end of file diff --git a/tests/end-to-end/package-lock.json b/tests/end-to-end/package-lock.json new file mode 100644 index 00000000..a50b1096 --- /dev/null +++ b/tests/end-to-end/package-lock.json @@ -0,0 +1,91 @@ +{ + "name": "end-to-end", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "end-to-end", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "@playwright/test": "^1.45.3", + "@types/node": "^22.1.0" + } + }, + "node_modules/@playwright/test": { + "version": "1.45.3", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.45.3.tgz", + "integrity": "sha512-UKF4XsBfy+u3MFWEH44hva1Q8Da28G6RFtR2+5saw+jgAFQV5yYnB1fu68Mz7fO+5GJF3wgwAIs0UelU8TxFrA==", + "dev": true, + "dependencies": { + "playwright": "1.45.3" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "22.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.1.0.tgz", + "integrity": "sha512-AOmuRF0R2/5j1knA3c6G3HOk523Ga+l+ZXltX8SF1+5oqcXijjfTd8fY3XRZqSihEu9XhtQnKYLmkFaoxgsJHw==", + "dev": true, + "dependencies": { + "undici-types": "~6.13.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.45.3", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.45.3.tgz", + "integrity": "sha512-QhVaS+lpluxCaioejDZ95l4Y4jSFCsBvl2UZkpeXlzxmqS+aABr5c82YmfMHrL6x27nvrvykJAFpkzT2eWdJww==", + "dev": true, + "dependencies": { + "playwright-core": "1.45.3" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.45.3", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.3.tgz", + "integrity": "sha512-+ym0jNbcjikaOwwSZycFbwkWgfruWvYlJfThKYAlImbxUgdWFO2oW70ojPm4OpE4t6TAo2FY/smM+hpVTtkhDA==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/undici-types": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.13.0.tgz", + "integrity": "sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==", + "dev": true + } + } +} diff --git a/tests/end-to-end/package.json b/tests/end-to-end/package.json new file mode 100644 index 00000000..2bba09a1 --- /dev/null +++ b/tests/end-to-end/package.json @@ -0,0 +1,14 @@ +{ + "name": "end-to-end", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": {}, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@playwright/test": "^1.45.3", + "@types/node": "^22.1.0" + } +} diff --git a/tests/end-to-end/playwright.config.ts b/tests/end-to-end/playwright.config.ts new file mode 100644 index 00000000..38423e13 --- /dev/null +++ b/tests/end-to-end/playwright.config.ts @@ -0,0 +1,78 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './.', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 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: 'http://127.0.0.1:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + // { + // name: 'firefox', + // use: { ...devices['Desktop Firefox'] }, + // }, + + // { + // name: 'webkit', + // use: { ...devices['Desktop Safari'] }, + // }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + // }, +});