Skip to content

Commit 64101c3

Browse files
authored
Merge pull request #16 from rstudio/refactor/playwright
2 parents 45c3e4d + 7977d56 commit 64101c3

File tree

11 files changed

+1176
-655
lines changed

11 files changed

+1176
-655
lines changed

.github/workflows/build.yml

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,34 @@ jobs:
3131
run: |
3232
make submodules
3333
34-
# =====================================================
35-
# Build
36-
# =====================================================
37-
# - uses: quarto-dev/quarto-actions/setup@v2
38-
3934
- name: Build shinylive
4035
run: |
4136
make all
4237
38+
# =====================================================
39+
# Run playwright tests
40+
# =====================================================
41+
- uses: actions/setup-node@v2
42+
with:
43+
node-version: "14.x"
44+
- name: Install dependencies
45+
run: yarn
46+
- name: Install Shinylive Python pacakge
47+
run: pip install shinylive
48+
- name: Install Playwright Browsers
49+
run: npx playwright install --with-deps
50+
- name: Run Playwright tests
51+
run: yarn playwright test
52+
- uses: actions/upload-artifact@v2
53+
if: always()
54+
with:
55+
name: playwright-report
56+
path: playwright-report/
57+
retention-days: 30
58+
59+
# =====================================================
60+
# Upload site/ artifact
61+
# =====================================================
4362
- name: Upload site/ artifact
4463
if: github.ref == 'refs/heads/main'
4564
uses: actions/upload-pages-artifact@v1
@@ -69,7 +88,6 @@ jobs:
6988
# =====================================================
7089
# Upload shinylive bundle on release
7190
# =====================================================
72-
7391
# Ensure that if the version in package.json is "0.0.5", then release tag
7492
# is "v0.0.5".
7593
- name: Check that version number matches release tag
@@ -96,6 +114,9 @@ jobs:
96114
overwrite: true
97115

98116

117+
# =====================================================
118+
# Deploy GitHub Pages site
119+
# =====================================================
99120
deploy_gh_pages:
100121
if: github.ref == 'refs/heads/main'
101122
needs: build

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@
66
__pycache__
77
/typings/
88

9+
/test-results/
10+
/playwright-report/
11+
/playwright/.cache/
12+
13+
.vscode/*.log
14+
15+
playwright/static-build/
16+
917
.pnp.*
1018
.yarn/*
1119
!.yarn/patches

package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
"url": "https://github.com/rstudio/shinylive",
99
"type": "git"
1010
},
11+
"scripts": {
12+
"serve": "tsx scripts/build.ts --serve",
13+
"test-server": "tsx scripts/build.ts --test-server",
14+
"cypress:open": "cypress open"
15+
},
1116
"author": "Winston Chang <winston@rstudio.com>",
1217
"license": "MIT",
1318
"devDependencies": {
@@ -25,6 +30,7 @@
2530
"@codemirror/view": "^6.0.0",
2631
"@github/clipboard-copy-element": "^1.1.2",
2732
"@lezer/common": "^1.0.0",
33+
"@playwright/test": "^1.25.0",
2834
"@testing-library/dom": "^8.13.0",
2935
"@testing-library/react": "^13.2.0",
3036
"@testing-library/user-event": "^14.2.0",

playwright.config.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import type { PlaywrightTestConfig } from "@playwright/test";
2+
import { devices } from "@playwright/test";
3+
4+
/**
5+
* Read environment variables from file.
6+
* https://github.com/motdotla/dotenv
7+
*/
8+
// require('dotenv').config();
9+
10+
/**
11+
* See https://playwright.dev/docs/test-configuration.
12+
*/
13+
const config: PlaywrightTestConfig = {
14+
testDir: "./playwright",
15+
/* Maximum time one test can run for. */
16+
timeout: 50 * 1000,
17+
expect: {
18+
/**
19+
* Maximum time expect() should wait for the condition to be met.
20+
* For example in `await expect(locator).toHaveText();`
21+
*/
22+
timeout: 20000,
23+
},
24+
/* Run tests in files in parallel */
25+
fullyParallel: true,
26+
/* Fail the build on CI if you accidentally left test.only in the source code. */
27+
forbidOnly: !!process.env.CI,
28+
/* Retry on CI only */
29+
retries: process.env.CI ? 2 : 1,
30+
/* Opt out of parallel tests on CI. */
31+
workers: process.env.CI ? 1 : undefined,
32+
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
33+
reporter: "html",
34+
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
35+
use: {
36+
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
37+
actionTimeout: 0,
38+
/* Base URL to use in actions like `await page.goto('/')`. */
39+
baseURL: "http://localhost:3000",
40+
41+
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
42+
trace: "on-first-retry",
43+
},
44+
45+
/* Configure projects for major browsers */
46+
projects: [
47+
{
48+
name: "chromium",
49+
use: {
50+
...devices["Desktop Chrome"],
51+
},
52+
},
53+
54+
// {
55+
// name: "firefox",
56+
// use: {
57+
// ...devices["Desktop Firefox"],
58+
// },
59+
// },
60+
61+
// {
62+
// name: "webkit",
63+
// use: {
64+
// ...devices["Desktop Safari"],
65+
// },
66+
// },
67+
],
68+
69+
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
70+
// outputDir: 'test-results/',
71+
72+
/* Run your local dev server before starting the tests */
73+
webServer: [
74+
{
75+
command: "yarn test-server",
76+
port: 3000,
77+
},
78+
{
79+
command:
80+
". venv/bin/activate && shinylive export playwright/static-app-test playwright/static-build && python3 -u -m http.server --directory playwright/static-build 8008 2> /dev/null",
81+
port: 8008,
82+
},
83+
],
84+
};
85+
86+
export default config;

playwright/examples-viewer.spec.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { expect_terminal_has_text, wait_until_initialized } from "./helpers";
2+
import { expect, test } from "@playwright/test";
3+
4+
test("Open examples page and click to a new example", async ({ page }) => {
5+
await page.goto("/examples/");
6+
// Click text=App with plot
7+
await page.locator("text=App with plot").click();
8+
await expect(page).toHaveURL("http://localhost:3000/examples/#app-with-plot");
9+
});
10+
11+
test("Add a new non-app script, type in it, and run code", async ({ page }) => {
12+
await page.goto("/examples/");
13+
14+
// Wait for initialization to complete
15+
await wait_until_initialized(page);
16+
await page.locator(`[aria-label="Add a file"]`).click();
17+
await page.locator('[aria-label="Name current file"]').fill("my_app.py");
18+
19+
await page.locator(".cm-editor [role=textbox]").type(`print("hello world")`);
20+
21+
// Running both command enter for mac and control enter for non-macs. Running
22+
// both just helps avoid looking at running environment
23+
await page.locator(".cm-editor [role=textbox]").press(`Meta+Enter`);
24+
await page.locator(".cm-editor [role=textbox]").press(`Control+Enter`);
25+
26+
// Make sure that hello world exists in the terminal output
27+
await expect_terminal_has_text(page, `>>> print("hello world")`);
28+
await expect_terminal_has_text(page, "hello world");
29+
});

playwright/helpers.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { Page, expect } from "@playwright/test";
2+
3+
/**
4+
* Wait until shinylive terminal shows the three >>>'s indicating that it's
5+
* ready to go
6+
* @param page Page object available from inside playwright tests
7+
*/
8+
export async function wait_until_initialized(page: Page) {
9+
await page.waitForSelector(`text=">>>"`, { timeout: 10000 });
10+
}
11+
12+
/**
13+
* Click the run app button in the upper right of the editor
14+
* @param page Page object available from inside playwright tests
15+
*/
16+
export async function click_run_app_button(page: Page) {
17+
// This is a bit of an intense selector for the run button but right now I
18+
// can't figure out a better way
19+
await page.locator(`[aria-label="Re-run code (Ctrl)-Shift-Enter"]`).click();
20+
}
21+
22+
async function expect_pane_has_text(
23+
page: Page,
24+
pane_selector: string,
25+
text: string
26+
) {
27+
await expect(page.locator(pane_selector, { hasText: text })).toBeVisible();
28+
}
29+
30+
/**
31+
* Test that some text exists in the editor pane
32+
* @param page Page object available from inside playwright tests
33+
* @param text Text to search for in the editor panel
34+
*/
35+
export async function expect_editor_has_text(page: Page, text: string) {
36+
await expect_pane_has_text(page, `.shinylive-editor`, text);
37+
}
38+
39+
/**
40+
* Test that some text exists in the terminal pane
41+
* @param page Page object available from inside playwright tests
42+
* @param text Text to search for in the terminal panel
43+
*/
44+
export async function expect_terminal_has_text(page: Page, text: string) {
45+
await expect_pane_has_text(page, `.shinylive-terminal`, text);
46+
}
47+
48+
/**
49+
* Test that some text exists in the app viewer pane
50+
* @param page Page object available from inside playwright tests
51+
* @param text Text to search for in the terminal panel
52+
* @param selector Element to search for text in. Defaults to h1 tag
53+
*/
54+
export async function expect_app_has_text(
55+
page: Page,
56+
text: string,
57+
selector: string = "h1"
58+
) {
59+
// For some reason there needs to be an await for the frame locator but not
60+
// for the other normal locators
61+
await expect(
62+
page.frameLocator(".app-frame").locator(selector, { hasText: text })
63+
).toBeVisible();
64+
}
65+
66+
/**
67+
* A URL data-hash containing the following app:
68+
*
69+
* ```
70+
* from shiny import App, render, ui
71+
*
72+
* app_ui = ui.page_fluid(
73+
* ui.h1("Code from a url")
74+
* )
75+
*
76+
* def server(input, output, session):
77+
* pass
78+
*
79+
* app = App(app_ui, server)
80+
* ```
81+
*
82+
*/
83+
export const app_url_encoding =
84+
"code=NobwRAdghgtgpmAXGKAHVA6VBPMAaMAYwHsIAXOcpMAMwCdiYACAZwAsBLCbJjmVYnTJMAgujxM6lACZw6EgK4cAOhFVpUAfSVMAvEyVYoAcziaaAGyXSAFKqYODHDGwCMdsAGFispvUZMUAZ0FspgAJSqkWoQsjSscgBucjZcqApkEsQZ6ZkJLCwcpOGI9o6oUAVlDuroeqLoNhraHBIsSXLRYAC+ALpAA";

playwright/load-from-url.spec.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import {
2+
app_url_encoding,
3+
expect_app_has_text,
4+
expect_editor_has_text,
5+
} from "./helpers";
6+
import { expect, test } from "@playwright/test";
7+
8+
test.describe("The URL can be used to load data", async () => {
9+
test("Editor view", async ({ page }) => {
10+
await page.goto(`/editor/#${app_url_encoding}`);
11+
12+
// Make sure the correct text is in the editor
13+
await expect_editor_has_text(page, 'ui.h1("Code from a url")');
14+
15+
await expect_app_has_text(page, "Code from a url", "h1");
16+
});
17+
18+
test("Examples view", async ({ page }) => {
19+
await page.goto(`/examples/#${app_url_encoding}`);
20+
21+
// Sanity check we're in the examples view.
22+
await expect(page.locator("text=/^examples$/i")).toBeVisible();
23+
24+
// Make sure the correct text is in the editor
25+
await expect_editor_has_text(page, 'ui.h1("Code from a url")');
26+
27+
await expect_app_has_text(page, "Code from a url", "h1");
28+
});
29+
30+
test("App view", async ({ page }) => {
31+
await page.goto(`/app/#${app_url_encoding}`);
32+
33+
await expect_app_has_text(page, "Code from a url", "h1");
34+
});
35+
});
36+
37+
// Looks for the shiny logo
38+
const header_bar_selector = '.HeaderBar img[alt="Shiny"]';
39+
40+
test.describe("The header bar parameter can turn off the header in app view but not the other views", async () => {
41+
test("Editor view can't turn off header bar", async ({ page }) => {
42+
await page.goto(`/editor/#h=0&${app_url_encoding}`);
43+
44+
await expect(page.locator(header_bar_selector)).toBeVisible();
45+
});
46+
test("Examples view can't turn off header bar", async ({ page }) => {
47+
await page.goto(`/examples/#h=0&${app_url_encoding}`);
48+
49+
await expect(page.locator(header_bar_selector)).toBeVisible();
50+
});
51+
52+
test("App view can turn off header bar", async ({ page }) => {
53+
await page.goto(`/app/#h=0&${app_url_encoding}`);
54+
55+
await expect(page.locator(header_bar_selector)).not.toBeVisible();
56+
});
57+
test("App view can show header bar", async ({ page }) => {
58+
await page.goto(`/editor/#${app_url_encoding}`);
59+
60+
await expect(page.locator(header_bar_selector)).toBeVisible();
61+
});
62+
});

0 commit comments

Comments
 (0)