Skip to content

Commit 6cc4cce

Browse files
committed
add e2e tests
1 parent c76610a commit 6cc4cce

File tree

10 files changed

+371
-72
lines changed

10 files changed

+371
-72
lines changed

.github/workflows/playwright.yml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
name: End to end Tests
2+
on:
3+
push:
4+
branches: [ main, master ]
5+
pull_request:
6+
branches: [ main, master ]
7+
jobs:
8+
test:
9+
timeout-minutes: 10
10+
defaults:
11+
run:
12+
working-directory: ./tests/end-to-end
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v4
16+
- name: Set up cargo cache
17+
uses: Swatinem/rust-cache@378c8285a4eaf12899d11bea686a763e906956af
18+
- uses: actions/setup-node@v4
19+
with:
20+
node-version: lts/*
21+
cache: 'npm'
22+
cache-dependency-path: ./tests/end-to-end/package-lock.json
23+
- name: Install dependencies
24+
run: >
25+
npm ci
26+
npx playwright install --with-deps chromium
27+
- name: build sqlpage
28+
run: cargo build
29+
working-directory: ./examples/official-site
30+
- name: start official site and wait for it to be ready
31+
timeout-minutes: 1
32+
run: >
33+
cargo run 2>/tmp/stderrlog &
34+
tail -f /tmp/stderrlog | grep -q "started successfully"
35+
working-directory: ./examples/official-site
36+
- name: Run Playwright tests
37+
run: npx playwright test
38+
- name: show server logs
39+
if: failure()
40+
run: cat /tmp/stderrlog
41+
- uses: actions/upload-artifact@v4
42+
if: always()
43+
with:
44+
name: playwright-report
45+
path: playwright-report/
46+
retention-days: 30

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## 0.26.0
44

5+
- 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.
56
- re-add a link to the website title in the shell component
67
- 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.
78
- allow loading javascript ESM modules in the shell component
@@ -14,6 +15,7 @@
1415
- fixed a bug where a form input with a value of `0` would diplay as empty instead of showing the `0`.
1516
- reduce the margin at the botton of forms to make them more compact.
1617
- fix [datagrid](https://sql.ophir.dev/documentation.sql?component=datagrid#component) color pills display when they contain long text.
18+
- fix the "started successfully" message being displayed before the error message when the server failed to start.
1719

1820
## 0.25.0 (2024-07-13)
1921

examples/official-site/documentation.sql

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ where $component is not null and not exists (select 1 from component where name
66
-- This line, at the top of the page, tells web browsers to keep the page locally in cache once they have it.
77
select 'http_header' as component, 'public, max-age=600, stale-while-revalidate=3600, stale-if-error=86400' as "Cache-Control";
88

9-
select 'dynamic' as component, properties FROM example WHERE component = 'shell' LIMIT 1;
9+
select 'dynamic' as component, json_patch(json_extract(properties, '$[0]'), json_object(
10+
'title', coalesce($component || ' - ', '') || 'SQLPage Documentation'
11+
)) as properties
12+
FROM example WHERE component = 'shell' LIMIT 1;
1013

1114
select 'text' as component, format('SQLPage v%s documentation', sqlpage.version()) as title;
1215
select '

examples/official-site/examples/handle_picture_upload.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ select 'Your picture' as title,
1010
'Uploaded file type: ' || sqlpage.uploaded_file_mime_type('my_file') as description
1111
where $data_url is not null;
1212

13-
select 'form' as component;
13+
select 'form' as component, 'Upload picture' as validate;
1414
select 'my_file' as name, 'file' as type, 'Picture' as label;
1515

1616
select 'text' as component, '

sqlpage/templates/shell.handlebars

Lines changed: 70 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@
7171
{{#if (or (or title (or icon image)) menu_item)}}
7272
<header id="sqlpage_header">
7373
<nav class="navbar navbar-expand-md navbar-light{{#if fixed_top_menu}} fixed-top{{/if}}">
74-
<div class="container-fluid gap-2">
74+
<div class="container-fluid gap-2 flex-nowrap justify-content-start" style="min-width:0">
7575
<a class="navbar-brand" href="{{#if link}}{{link}}{{else}}/{{/if}}">
7676
{{#if image}}
7777
<img src="{{image}}" alt="{{title}}" width="32" height="32"
@@ -81,83 +81,83 @@
8181
{{~icon_img icon~}}
8282
{{/if}}
8383
</a>
84-
{{#if title}}<h1 class="mb-0 fs-2 text-truncate"><a class="navbar-brand" href="{{#if link}}{{link}}{{else}}/{{/if}}">{{title}}</a></h1>{{/if}}
84+
{{#if title}}<h1 class="mb-0 fs-2 text-truncate flex-grow-1"><a href="{{#if link}}{{link}}{{else}}/{{/if}}">{{title}}</a></h1>{{/if}}
8585
<button class="navbar-toggler" type="button" data-bs-toggle="collapse"
8686
data-bs-target="#navbar-menu" aria-controls="navbar-menu" aria-expanded="false"
8787
aria-label="Toggle navigation">
8888
<span class="navbar-toggler-icon"></span>
8989
</button>
90-
<div class="collapse navbar-collapse" id="navbar-menu">
91-
<ul class="navbar-nav ms-auto">
92-
{{~#each (to_array menu_item)~}}
93-
{{~#if (or (eq (typeof this) 'object') (and (eq (typeof this) 'string') (starts_with this '{')))}}
94-
{{~#with (parse_json this)}}
95-
{{#if (or (or this.title this.icon) this.image)}}
96-
<li class="nav-item{{#if this.submenu}} dropdown{{/if}}">
97-
<a class="nav-link {{#if this.submenu}}dropdown-toggle{{/if}}" href="{{#if this.link}}{{this.link}}{{else}}#{{/if}}"
98-
{{~#if this.submenu}} data-bs-toggle="dropdown" data-bs-auto-close="outside" {{/if~}}
99-
role="button"
100-
>
101-
{{~#if this.image~}}
102-
<span {{~#if this.title}} class="me-1"{{/if}}>
103-
{{~#if (eq this.size 'sm')}}
104-
<img width=16 height=16 src="{{this.image}}">
105-
{{~else~}}
106-
<img width=24 height=24 src="{{this.image}}">
107-
{{~/if~}}
108-
</span>
109-
{{~/if~}}
110-
{{#if this.icon}}
111-
{{#if this.title}}<span class="me-1">{{/if}}
112-
{{~icon_img this.icon~}}
113-
{{#if this.title}}</span>{{/if}}
114-
{{/if}}
115-
{{~this.title~}}
116-
</a>
117-
{{~#if this.submenu~}}
118-
<div class="dropdown-menu dropdown-menu-end" data-bs-popper="static">
119-
{{~#each this.submenu~}}
120-
{{#if (or (or this.title this.icon) this.image)}}
121-
<a class="dropdown-item" href="{{this.link}}">
122-
{{~#if this.image~}}
123-
<span {{~#if this.title}} class="me-1"{{/if}}>
124-
{{~#if (eq ../this.size 'sm')}}
125-
<img width=16 height=16 src="{{this.image}}">
126-
{{~else~}}
127-
<img width=24 height=24 src="{{this.image}}">
128-
{{~/if~}}
129-
</span>
130-
{{~/if~}}
131-
{{#if this.icon}}
132-
{{#if this.title}}<span class="me-1">{{/if}}
133-
{{~icon_img this.icon~}}
134-
{{#if this.title}}</span>{{/if}}
135-
{{/if}}
136-
{{~this.title~}}
137-
</a>
138-
{{~/if~}}
139-
{{~/each~}}
140-
</div>
90+
</div>
91+
<div class="collapse navbar-collapse" id="navbar-menu">
92+
<ul class="navbar-nav ms-auto">
93+
{{~#each (to_array menu_item)~}}
94+
{{~#if (or (eq (typeof this) 'object') (and (eq (typeof this) 'string') (starts_with this '{')))}}
95+
{{~#with (parse_json this)}}
96+
{{#if (or (or this.title this.icon) this.image)}}
97+
<li class="nav-item{{#if this.submenu}} dropdown{{/if}}">
98+
<a class="nav-link {{#if this.submenu}}dropdown-toggle{{/if}}" href="{{#if this.link}}{{this.link}}{{else}}#{{/if}}"
99+
{{~#if this.submenu}} data-bs-toggle="dropdown" data-bs-auto-close="outside" {{/if~}}
100+
role="button"
101+
>
102+
{{~#if this.image~}}
103+
<span {{~#if this.title}} class="me-1"{{/if}}>
104+
{{~#if (eq this.size 'sm')}}
105+
<img width=16 height=16 src="{{this.image}}">
106+
{{~else~}}
107+
<img width=24 height=24 src="{{this.image}}">
108+
{{~/if~}}
109+
</span>
110+
{{~/if~}}
111+
{{#if this.icon}}
112+
{{#if this.title}}<span class="me-1">{{/if}}
113+
{{~icon_img this.icon~}}
114+
{{#if this.title}}</span>{{/if}}
141115
{{/if}}
142-
</li>
143-
{{/if}}
144-
{{/with}}
145-
{{~else}}
146-
{{~#if (gt (len this) 0)~}}
147-
<li class="nav-item">
148-
<a class="nav-link text-capitalize" href="{{this}}.sql">{{this}}</a>
116+
{{~this.title~}}
117+
</a>
118+
{{~#if this.submenu~}}
119+
<div class="dropdown-menu dropdown-menu-end" data-bs-popper="static">
120+
{{~#each this.submenu~}}
121+
{{#if (or (or this.title this.icon) this.image)}}
122+
<a class="dropdown-item" href="{{this.link}}">
123+
{{~#if this.image~}}
124+
<span {{~#if this.title}} class="me-1"{{/if}}>
125+
{{~#if (eq ../this.size 'sm')}}
126+
<img width=16 height=16 src="{{this.image}}">
127+
{{~else~}}
128+
<img width=24 height=24 src="{{this.image}}">
129+
{{~/if~}}
130+
</span>
131+
{{~/if~}}
132+
{{#if this.icon}}
133+
{{#if this.title}}<span class="me-1">{{/if}}
134+
{{~icon_img this.icon~}}
135+
{{#if this.title}}</span>{{/if}}
136+
{{/if}}
137+
{{~this.title~}}
138+
</a>
139+
{{~/if~}}
140+
{{~/each~}}
141+
</div>
142+
{{/if}}
149143
</li>
150-
{{~/if~}}
144+
{{/if}}
145+
{{/with}}
146+
{{~else}}
147+
{{~#if (gt (len this) 0)~}}
148+
<li class="nav-item">
149+
<a class="nav-link text-capitalize" href="{{this}}.sql">{{this}}</a>
150+
</li>
151151
{{~/if~}}
152-
{{~/each}}
153-
</ul>
154-
{{#if search_target}}
155-
<form class="d-flex" role="search" action="{{search_target}}">
156-
<input class="form-control me-2" type="search" placeholder="Search" aria-label="Search" name="search" value="{{search_value}}">
157-
<button class="btn btn-outline-success" type="submit">Search</button>
158-
</form>
159-
{{/if}}
160-
</div>
152+
{{~/if~}}
153+
{{~/each}}
154+
</ul>
155+
{{#if search_target}}
156+
<form class="d-flex" role="search" action="{{search_target}}">
157+
<input class="form-control me-2" type="search" placeholder="Search" aria-label="Search" name="search" value="{{search_value}}">
158+
<button class="btn btn-outline-success" type="submit">Search</button>
159+
</form>
160+
{{/if}}
161161
</div>
162162
</nav>
163163
</header>

tests/end-to-end/.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules/
2+
/test-results/
3+
/playwright-report/
4+
/blob-report/
5+
/playwright/.cache/
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { test, expect } from '@playwright/test';
2+
3+
const BASE = 'http://localhost:8080/';
4+
5+
test('Open documentation', async ({ page }) => {
6+
await page.goto(BASE);
7+
8+
// Expect a title "to contain" a substring.
9+
await expect(page).toHaveTitle("SQLPage");
10+
11+
// open the submenu
12+
await page.getByText('Documentation', { exact: true }).first().click();
13+
await page.getByText('All Components').click();
14+
const components = ['form', 'map', 'chart', 'button'];
15+
for (const component of components) {
16+
await expect(page.getByRole('link', { name: component }).first()).toBeVisible();
17+
}
18+
});
19+
20+
test('chart', async ({ page }) => {
21+
await page.goto(BASE + '/documentation.sql?component=chart#component');
22+
await expect(page.getByText('Loading...')).not.toBeVisible();
23+
await expect(page.locator('.apexcharts-canvas').first()).toBeVisible();
24+
});
25+
26+
test('map', async ({ page }) => {
27+
await page.goto(BASE + '/documentation.sql?component=map#component');
28+
await expect(page.getByText('Loading...')).not.toBeVisible();
29+
await expect(page.locator('.leaflet-marker-icon').first()).toBeVisible();
30+
});
31+
32+
test('form example', async ({ page }) => {
33+
await page.goto(BASE + '/examples/multistep-form');
34+
// Single selection matching the value or label
35+
await page.getByLabel('From').selectOption('Paris');
36+
await page.getByText('Next').click();
37+
await page.getByLabel(/\bTo\b/).selectOption('Mexico');
38+
await page.getByText('Next').click();
39+
await page.getByLabel('Number of Adults').fill('1');
40+
await page.getByText('Next').click();
41+
await page.getByLabel('Passenger 1 (adult)').fill('John Doe');
42+
await page.getByText('Book the flight').click();
43+
await expect(page.getByText('John Doe').first()).toBeVisible();
44+
});
45+
46+
test('File upload', async ({ page }) => {
47+
await page.goto(BASE);
48+
await page.getByText('Examples', { exact: true }).click();
49+
await page.getByText('File uploads').click();
50+
const my_svg = '<svg><text y="20">Hello World</text></svg>';
51+
// @ts-ignore
52+
const buffer = Buffer.from(my_svg);
53+
await page.getByLabel('Picture').setInputFiles({
54+
name: 'small.svg',
55+
mimeType: 'image/svg+xml',
56+
buffer,
57+
});
58+
await page.getByRole('button', { name: 'Upload picture' }).click();
59+
await expect(page.locator('img[src^=data]').first().getAttribute('src')).resolves.toBe('data:image/svg+xml;base64,' + buffer.toString('base64'));
60+
});

0 commit comments

Comments
 (0)