diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0c1ae927e..cf3928fb4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -127,7 +127,7 @@ jobs: path: | site viz - - name: Archive Javascript app + - name: Archive linux electron app uses: actions/upload-artifact@v4 with: name: linux @@ -193,7 +193,11 @@ jobs: webapp-playwright-test: needs: [cli-and-doc, webapp-ubuntu] - if: github.ref == 'refs/heads/master' + # run only on master OR when the PR is _not_ a draft + # TODO: improve this? + if: | + github.ref == 'refs/heads/master' || + github.event.pull_request.draft == false timeout-minutes: 75 runs-on: ubuntu-latest steps: @@ -206,6 +210,7 @@ jobs: npm ci sudo apt-get update sudo apt-get install lighttpd + sudo apt-get install xvfb x11-xkb-utils xfonts-100dpi xfonts-75dpi xfonts-scalable xfonts-cyrillic x11-apps sudo npm install -D @playwright/test@latest npm install --save-dev @types/node @types/yauzl - name: Install Playwright Browsers @@ -225,14 +230,36 @@ jobs: ' > lighttpd.conf lighttpd -f lighttpd.conf -D & sleep 10 && curl -i http://127.0.0.1:12345/index.html # test + - name: Download electron app + uses: actions/download-artifact@v4 + with: + name: linux + # - name: Setup electron app + # run: | + # tar xzf Kappapp.tar.gz + # mkdir -p build + # mv Kappapp ./build/ + # # pwd + # # ls -R + # # ls -R /home/runner/work/ + # Xvfb :99 -ac -screen 0 1920x1080x24 +extension GLX +render > xvfb_log.txt 2>&1 & + # sleep 3 - name: Run Playwright tests - run: DEBUG=pw:webserver npx playwright test --retries=3 --trace retain-on-first-failure + run: | + # export DISPLAY=:99 # needed for electron + DEBUG=pw:browser* npx playwright test --retries=2 --trace retain-on-first-failure - uses: actions/upload-artifact@v4 if: ${{ !cancelled() }} with: name: playwright-report path: playwright-report/ retention-days: 30 + # - uses: actions/upload-artifact@v4 + # if: ${{ !cancelled() }} + # with: + # name: xvfb_log + # path: xvfb_log.txt + # retention-days: 30 deploy: needs: [cli-and-doc, webapp-ubuntu, webapp-macos, webapp-windows, webapp-playwright-test] diff --git a/gui/entry_point/main.js b/gui/entry_point/main.js index 75be82193..885b3589e 100644 --- a/gui/entry_point/main.js +++ b/gui/entry_point/main.js @@ -1,12 +1,14 @@ "use strict"; // Modules to control application life and create native browser window -const {app, BrowserWindow} = require('electron') +const { app, BrowserWindow } = require('electron') // Keep a global reference of the window object, if you don't, the window will // be closed automatically when the JavaScript object is garbage collected. let mainWindow -function createWindow () { +// Don't open the window rightaway (with show:false below) +// See https://zeke.github.io/electron.atom.io/docs/api/browser-window/#using-ready-to-show-event +function createWindow() { // Create the browser window. mainWindow = new BrowserWindow({ webPreferences: { @@ -14,7 +16,8 @@ function createWindow () { contextIsolation: false, }, width: 1024, - height: 768 + height: 768, + show: false, }) // and load the index.html of the app. @@ -24,7 +27,7 @@ function createWindow () { pathname: require('path').join(__dirname, '../bin/KappaSwitchman'), query: { label: 'Local' } }) - mainWindow.loadFile('index.html',{ + mainWindow.loadFile('index.html', { query: { host: sim_agent, // level: "debug", @@ -35,16 +38,21 @@ function createWindow () { // mainWindow.webContents.openDevTools() // Emitted when the window is closed. - mainWindow.on('closed', function () { + mainWindow.on('closed', function() { // Dereference the window object, usually you would store windows // in an array if your app supports multi windows, this is the time // when you should delete the corresponding element. mainWindow = null }) + + mainWindow.once('ready-to-show', () => { + // TODO: put this back conditionnally + // mainWindow.show() + }) } // Quit when all windows are closed. -app.on('window-all-closed', function () { +app.on('window-all-closed', function() { // On macOS it is common for applications and their menu bar // to stay active until the user quits explicitly with Cmd + Q if (process.platform !== 'darwin') { @@ -52,7 +60,7 @@ app.on('window-all-closed', function () { } }) -app.on('activate', function () { +app.on('activate', function() { // On macOS it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. if (mainWindow === null) { diff --git a/gui/resources/JsSim.css b/gui/resources/JsSim.css index 06a556f95..5983d6f73 100644 --- a/gui/resources/JsSim.css +++ b/gui/resources/JsSim.css @@ -174,7 +174,7 @@ rect { stroke-width: 2; } } .plot-legend-swatch { stroke-width : 1; } -.plot-tick-proof { } +/* .plot-tick-proof { } */ .contact-tooltip { position: fixed; z-index: 10; padding: 2px; border-style: solid; diff --git a/playwright.config.ts b/playwright.config.ts index 39d4554bd..ae25e6edd 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,5 +1,7 @@ import { defineConfig, devices } from '@playwright/test'; +import type { TestOptions } from './tests/playwright/project_electron_param'; + /** * Read environment variables from file. * https://github.com/motdotla/dotenv @@ -11,7 +13,7 @@ import { defineConfig, devices } from '@playwright/test'; /** * See https://playwright.dev/docs/test-configuration. */ -export default defineConfig({ +export default defineConfig({ testDir: './tests/playwright', /* Run tests in files in parallel */ fullyParallel: true, @@ -33,6 +35,7 @@ export default defineConfig({ }, /* Configure projects for major browsers */ + // See https://playwright.dev/docs/test-parameterize#parameterized-projects for electron param definition projects: [ { name: 'chromium', @@ -44,6 +47,12 @@ export default defineConfig({ use: { ...devices['Desktop Firefox'] }, }, + // TODO: try having electron tests working + // { + // name: 'electron', + // use: { run_in_electron: true }, + // }, + // TODO: try making the app work on webkit /* { diff --git a/tests/playwright/README.md b/tests/playwright/README.md index 771360579..afc9bc970 100644 --- a/tests/playwright/README.md +++ b/tests/playwright/README.md @@ -24,3 +24,11 @@ UPDATE_EXPORTS=true npx playwright test --update-snapshots UPDATE_EXPORTS=true npx playwright test --update-snapshots --project firefox UPDATE_EXPORTS=true npx playwright test procedure.spec.ts:449 --update-snapshots ``` + +### Electron + +Local tests with electron need to have access to the actual screen, it seems. +So they should be run with `-j 1` to have a single worker, and the electron window has to stay visible at least. + +Downloads open a dialog that is not handled by playwright, so there is no testing of electron downloads. +They may be enabled and completed manually by changing the boolean in `project_electron_param.ts` (untested). diff --git a/tests/playwright/procedure.spec.ts b/tests/playwright/procedure.spec.ts index 859d98767..0845e7bfb 100644 --- a/tests/playwright/procedure.spec.ts +++ b/tests/playwright/procedure.spec.ts @@ -2,8 +2,10 @@ // Note: trace snapshots that should be taken by playwright are not (absent on right of ui `npx playwright test --ui`) // TODO: test with embedded Kasim? deprecate it? +// TODO: split this in multiple files? -import { test, expect, type Page } from '@playwright/test'; +import { expect, type Page } from '@playwright/test'; +import { test, RUN_DOWNLOADS_IN_ELECTRON } from './project_electron_param'; import * as utils from './webapp_utils'; @@ -20,13 +22,19 @@ const abc_ka = '//raw.githubusercontent.com/Kappa-Dev/KappaTools/master/examples const poly_ka = '//raw.githubusercontent.com/Kappa-Dev/KappaTools/master/examples/poly.ka' const local_views_slide_69_ka = '//www.di.ens.fr/~feret/teaching/2023-2024/MPRI.2.19/activities/local_views/local_views_slide_69.ka' const counter_2_ka = '//raw.githubusercontent.com/Kappa-Dev/KappaTools/master/tests/integration/compiler/counters_2_levels/counter_2.ka' -const minikai_counters_ka = '//raw.githubusercontent.com/Kappa-Dev/KappaTools/master/examples/large/minikai/minikai_counters.ka' const causality_slide_10_ka = '//www.di.ens.fr/~feret/teaching/2023-2024/MPRI.2.19/activities/causality/causality_slide_10.ka' test.describe('Editor tab', () => { - test('editor', async ({ page }) => { - await utils.open_app_with_model(page, abc_ka); + test('open_file_from_url', async ({ page, run_in_electron }) => { + test.skip(run_in_electron, "Not relevant in electron: opening url would use the server instead of the electron app."); + + await utils.open_app_with_model(page, abc_ka, run_in_electron, false); + }); + + + test('editor', async ({ page, run_in_electron }) => { + await utils.open_app_with_model(page, abc_ka, run_in_electron); const editor = page.locator('#editor-panel').getByRole('textbox'); async function editor_to_line(n: number): Promise { @@ -40,9 +48,9 @@ test.describe('Editor tab', () => { await editor_to_line(21); // Make a syntax error await editor.press('Backspace'); - // (useless comment to match brackets { {) + // (useless comment to match brackets in editor { ) await utils.expect_error(page, [ - " « 1/1 » [abc.ka] invalid internal state or missing '}' " + " « 1/1 » [model.ka] invalid internal state or missing '}' " ]); await editor_cancel(); await utils.expect_no_error(page); @@ -50,7 +58,7 @@ test.describe('Editor tab', () => { await editor.fill('\n%agent: D(a{u p})'); await utils.expect_error(page, [ - " « 1/1 » [abc.ka] Dead agent D " + " « 1/1 » [model.ka] Dead agent D " ]); await editor_to_line(25); @@ -61,7 +69,7 @@ test.describe('Editor tab', () => { await editor.fill("\n'd' D(a{p}) -> D(a{u}) @ 1"); // await page.locator('#panel_preferences_message_nav_inc_id').click(); await utils.expect_error(page, [ - " « 1/1 » [abc.ka] Dead rule 'd' " + " « 1/1 » [model.ka] Dead rule 'd' " ]); await editor_cancel(); await editor_cancel(); @@ -69,9 +77,9 @@ test.describe('Editor tab', () => { await utils.expect_no_error(page); }); - test('contact_map', async ({ page, browserName }) => { + test('contact_map', async ({ page, run_in_electron, browserName }) => { + await utils.open_app_with_model(page, abc_ka, run_in_electron); const opts_screen = { maxDiffPixels: 60 } - await utils.open_app_with_model(page, abc_ka); const contact_map = page.locator('#map-container'); await expect.soft(contact_map).toHaveScreenshot(opts_screen); await page.getByRole('checkbox', { name: 'Interactive Mode' }).check(); @@ -89,18 +97,19 @@ test.describe('Editor tab', () => { await page.getByRole('button', { name: 'Reset Zoom' }).click(); await expect.soft(contact_map).toHaveScreenshot(opts_screen); - await utils.testExports(page, '#export_contact-export', 'map', ['svg', 'json'], undefined, browserName); - - // await utils.testExports(page, '#export_contact-export', 'map', ['png'], undefined, browserName); - // TODO: pngs doesn't match on CI's chromium and firefox. check if we can test them in some way. + if (!run_in_electron || RUN_DOWNLOADS_IN_ELECTRON) { + await utils.testExports(page, '#export_contact-export', 'map', ['svg', 'json'], undefined, browserName); + // await utils.testExports(page, '#export_contact-export', 'map', ['png'], undefined, browserName); + // TODO: pngs doesn't match on CI's chromium and firefox. check if we can test them in some way. + } }); - test('influences', async ({ page }) => { + test('influences', async ({ page, run_in_electron }) => { const opts_screen = { maxDiffPixels: 60 } const opts_screen_lenient = { maxDiffPixels: 150, threshold: 0.4 } - await utils.open_app_with_model(page, abc_ka); + await utils.open_app_with_model(page, abc_ka, run_in_electron); await page.locator('#navinfluences').click(); const table = page.locator('#influences-table'); @@ -129,15 +138,17 @@ test.describe('Editor tab', () => { await page.getByRole('button', { name: 'Previous' }).click(); await expect.soft(table).toHaveScreenshot(); //export - await utils.testExports(page, '#export_influence-export', 'influences', ['json']); + if (!run_in_electron || RUN_DOWNLOADS_IN_ELECTRON) { + await utils.testExports(page, '#export_influence-export', 'influences', ['json']); + } }); function constraint_locator(page: Page, n: number) { return (page.locator('#constraints > .panel-scroll > div > .panel-body').nth(n)); } - test('constraints_and_polymers_1', async ({ page }) => { - await utils.open_app_with_model(page, abc_ka); + test('constraints_and_polymers_1', async ({ page, run_in_electron }) => { + await utils.open_app_with_model(page, abc_ka, run_in_electron); await page.locator('#navconstraints').click(); await expect.soft(constraint_locator(page, 0)).toHaveText( `A(c) => [ A(c[.]) v A(c[x1.C]) v A(c[x2.C]) ] @@ -159,8 +170,8 @@ C(x2) => [ C(x2{u}) v C(x2{p}) ] ); }); - test('constraints_and_polymers_2', async ({ page }) => { - await utils.open_app_with_model(page, poly_ka); + test('constraints_and_polymers_2', async ({ page, run_in_electron }) => { + await utils.open_app_with_model(page, poly_ka, run_in_electron); await page.locator('#navpolymers').click(); await expect.soft(page.getByRole('paragraph')).toHaveText( `The following bonds may form arbitrary long chains of agents: @@ -176,8 +187,8 @@ A(c[1]),C(a[1]) `); }); - test('constraints_and_polymers_3', async ({ page }) => { - await utils.open_app_with_model(page, local_views_slide_69_ka, true, 20000); + test('constraints_and_polymers_3', async ({ page, run_in_electron }) => { + await utils.open_app_with_model(page, local_views_slide_69_ka, run_in_electron, true, 20000); await page.locator('#navconstraints').click(); await expect.soft(constraint_locator(page, 0)).toHaveText( `E(x) => [ E(x[.]) v E(x[x.R]) ] @@ -212,8 +223,8 @@ R(CN[C.R],CR[CR.R]) => R(CN[2],CR[1]),R(C[2],CR[1]) ]); }); - test('constraints_and_polymers_4', async ({ page }) => { - await utils.open_app_with_model(page, counter_2_ka); + test('constraints_and_polymers_4', async ({ page, run_in_electron }) => { + await utils.open_app_with_model(page, counter_2_ka, run_in_electron); await page.locator('#navconstraints').click(); await expect.soft(constraint_locator(page, 4)).toHaveText( `A() => A(c{[0 .. 2]}) @@ -221,10 +232,17 @@ R(CN[C.R],CR[CR.R]) => R(CN[2],CR[1]),R(C[2],CR[1]) ); }); - test('contact_map_accuracy', async ({ page }) => { - // TODO: find a smaller example so that execution is faster - test.setTimeout(180000) - await utils.open_app_with_model(page, minikai_counters_ka, false, 120000); + test('contact_map_accuracy', async ({ page, run_in_electron }) => { + await utils.open_app_with_model_from_text(page, + `%agent: A(x,c,d) +%agent: B(x,y) +%agent: C(x1{u p},x2{u p}) +'a.b' A(x[.]),B(x[.]) -> A(x[1]),B(x[1]) @ 'on_rate' //A binds B +'a..b' A(x[1/.]),B(x[1/.]) @ 'off_rate' //AB dissociation +'never_occuring' A(d[1]),B(y[1]) -> A(d[.]), B(y[.]) @ 1 +%init: 12 A(),B() +%init: 13 C()` + , run_in_electron); const contact_map = page.locator('#map-container'); await expect.soft(contact_map).toHaveScreenshot(); await page.locator('#contact_map-accuracy').selectOption('high'); @@ -235,8 +253,8 @@ R(CN[C.R],CR[CR.R]) => R(CN[2],CR[1]),R(C[2],CR[1]) test.describe('Simulation tools', () => { - test('Simulation, plot', async ({ page, browserName }) => { - await utils.open_app_with_model(page, abc_ka); + test('Simulation, plot', async ({ page, run_in_electron, browserName }) => { + await utils.open_app_with_model(page, abc_ka, run_in_electron); await utils.setSeed(page, 1); // Run simulation to 30, then 100, then test plot options await utils.set_pause_if(page, '[T] > 30'); @@ -271,11 +289,13 @@ test.describe('Simulation tools', () => { await page.locator('.panel-footer').click(); // needed for update await expect.soft(page.getByRole('img')).toHaveScreenshot(); - await utils.testExports(page, '#export_plot-export', 'plot', ['csv', 'json', 'tsv'], undefined); - await utils.testExports(page, '#export_plot-export', 'plot', ['svg'], undefined, browserName); - if (browserName != "chromium") { - await utils.testExports(page, '#export_plot-export', 'plot', ['png'], undefined, browserName); - // TODO: pngs doesn't match on CI's chromium. check if we can test them in some way + if (!run_in_electron || RUN_DOWNLOADS_IN_ELECTRON) { + await utils.testExports(page, '#export_plot-export', 'plot', ['csv', 'json', 'tsv'], undefined); + await utils.testExports(page, '#export_plot-export', 'plot', ['svg'], undefined, browserName); + if (browserName != "chromium") { + await utils.testExports(page, '#export_plot-export', 'plot', ['png'], undefined, browserName); + // TODO: pngs doesn't match on CI's chromium. check if we can test them in some way + } } // Test larger plots, slider @@ -296,8 +316,8 @@ test.describe('Simulation tools', () => { await page.getByRole('button', { name: 'pause' }).click(); }); - test('DIN', async ({ page, browserName }) => { - await utils.open_app_with_model(page, abc_ka); + test('DIN', async ({ page, run_in_electron, browserName }) => { + await utils.open_app_with_model(page, abc_ka, run_in_electron); await utils.setSeed(page, 1); async function expectScreenShotDINTable(chromium_maxDiffPixels: number = 0) { @@ -326,7 +346,9 @@ test.describe('Simulation tools', () => { await page.locator('#navDIN').click(); await expectScreenShotDINTable(350); - await utils.testExports(page, '#export_din-export', 'flux', ['json', 'dot', 'html']); + if (!run_in_electron || RUN_DOWNLOADS_IN_ELECTRON) { + await utils.testExports(page, '#export_din-export', 'flux', ['json', 'dot', 'html']); + } await utils.set_pause_if(page, '[T] > 60'); await page.getByRole('button', { name: 'continue' }).click(); @@ -338,14 +360,16 @@ test.describe('Simulation tools', () => { await page.getByRole('combobox').first().selectOption('flux.json'); await expectScreenShotDINTable(3000); - await utils.testExports(page, '#export_din-export', 'flux_json', ['json', 'dot', 'html']); + if (!run_in_electron || RUN_DOWNLOADS_IN_ELECTRON) { + await utils.testExports(page, '#export_din-export', 'flux_json', ['json', 'dot', 'html']); + } await page.getByRole('combobox').first().selectOption('flux.html'); await expectScreenShotDINTable(350); }); - test('snapshots', async ({ page, browserName }) => { - await utils.open_app_with_model(page, abc_ka); + test('snapshots', async ({ page, run_in_electron, browserName }) => { + await utils.open_app_with_model(page, abc_ka, run_in_electron); await utils.setSeed(page, 1); // Generate two snapshots @@ -430,28 +454,30 @@ test.describe('Simulation tools', () => { // await page.locator('#force-container circle').first().click(); // Test exports - await page.locator('#format_select_id').selectOption('Kappa'); - await utils.testExports(page, "#export_snapshot_kappa", "snapshot_kappa", ["json", "kappa", "dot"], - ['', '', '"#\\w{5,6}"']); - await page.locator('#format_select_id').selectOption('Graph'); - await utils.testExports(page, "#export_snapshot_graph", "snapshot_graph", ["json", "kappa", "dot"], ['', '', '"#\\w{5,6}"']); - - await utils.testExports(page, "#export_snapshot_graph", "snapshot_graph", ["svg"], - [' { - await utils.open_app_with_model(page, abc_ka); + test('outputs', async ({ page, run_in_electron }) => { + await utils.open_app_with_model(page, abc_ka, run_in_electron); await utils.setSeed(page, 1); // Generate two snapshots @@ -486,19 +512,20 @@ test.describe('Simulation tools', () => { await page.locator('#output-select-id').selectOption('ab.txt'); await expect.soft(outputs_display).toHaveText("394393"); - const downloadPromise = page.waitForEvent('download'); - await page.getByRole('button', { name: 'All outputs' }).click(); - const download = await downloadPromise; - - utils.compare_zip_files_list_with_ref(download, []); + if (!run_in_electron || RUN_DOWNLOADS_IN_ELECTRON) { + const downloadPromise = page.waitForEvent('download'); + await page.getByRole('button', { name: 'All outputs' }).click(); + const download = await downloadPromise; + utils.compare_zip_files_list_with_ref(download, []); + } }); }); test.describe('stories', () => { - async function setup_stories(page: Page) { - await utils.open_app_with_model(page, causality_slide_10_ka, true); + async function setup_stories(page: Page, run_in_electron: boolean) { + await utils.open_app_with_model(page, causality_slide_10_ka, run_in_electron, true); await utils.setSeed(page, 1); // Enable trace @@ -540,8 +567,8 @@ test.describe('stories', () => { } - test('Empty', async ({ page }) => { - await setup_stories(page); + test('Empty', async ({ page, run_in_electron }) => { + await setup_stories(page, run_in_electron); // No screenshot test as no stories causes no image to locate await computeStoriesAndTest(page, "", `Starting Compression Compression completed @@ -549,8 +576,8 @@ Compression completed `, false); }); - test('Weakly', async ({ page }) => { - await setup_stories(page); + test('Weakly', async ({ page, run_in_electron }) => { + await setup_stories(page, run_in_electron); await page.getByRole('checkbox', { name: 'Weakly' }).check(); await computeStoriesAndTest(page, `ids: 11, 19, 24, 29, 33, 36, 37, 39, 49, 52, 55 @@ -567,8 +594,8 @@ Compression completed `); }); - test('Strongly', async ({ page }) => { - await setup_stories(page); + test('Strongly', async ({ page, run_in_electron }) => { + await setup_stories(page, run_in_electron); await page.getByRole('checkbox', { name: 'Strongly' }).check(); await computeStoriesAndTest(page, `ids: 11, 19, 24, 29, 33, 36, 37, 39, 49, 52, 55, 5, 8, 21, 27, 28, 30, 31, @@ -603,8 +630,8 @@ Compression completed }); - test('Causal + select stories', async ({ page }) => { - await setup_stories(page); + test('Causal + select stories', async ({ page, run_in_electron }) => { + await setup_stories(page, run_in_electron); await page.getByRole('checkbox', { name: 'Causal' }).check(); const computation_log = `Starting Compression Start one causal compression @@ -689,8 +716,8 @@ event=5200, 3184, 2246`, computation_log); }); - test('Weakly + Strongly', async ({ page }) => { - await setup_stories(page); + test('Weakly + Strongly', async ({ page, run_in_electron }) => { + await setup_stories(page, run_in_electron); await page.getByRole('checkbox', { name: 'Weakly' }).check(); await page.getByRole('checkbox', { name: 'Strongly' }).check(); await computeStoriesAndTest(page, @@ -712,19 +739,21 @@ Compression completed `); }); - test('Trace download', async ({ page }) => { - await setup_stories(page); - const downloadPromise = page.waitForEvent('download'); - await page.getByRole('button', { name: 'get trace' }).click(); - const download = await downloadPromise; - await utils.compare_download_to_ref(download, "stories_trace"); + test('Trace download', async ({ page, run_in_electron }) => { + await setup_stories(page, run_in_electron); + if (!run_in_electron || RUN_DOWNLOADS_IN_ELECTRON) { + const downloadPromise = page.waitForEvent('download'); + await page.getByRole('button', { name: 'get trace' }).click(); + const download = await downloadPromise; + await utils.compare_download_to_ref(download, "stories_trace"); + } }); }); test.describe('projects_and_files', () => { - test('project', async ({ page }) => { - await utils.open_app_with_model(page, causality_slide_10_ka, true); + test('project', async ({ page, run_in_electron }) => { + await utils.open_app_with_model(page, causality_slide_10_ka, run_in_electron, true); await utils.setSeed(page, 1); // project tab is `a` in `list`, `list` contains "active" class info, `a` is clickable @@ -770,36 +799,38 @@ test.describe('projects_and_files', () => { // TODO: Could also check simulation results }); - test('files', async ({ page }) => { - await utils.open_app_with_model(page, abc_ka, false); + test('files', async ({ page, run_in_electron }) => { + await utils.open_app_with_model(page, abc_ka, run_in_electron, false); await utils.setSeed(page, 1); - // download file - await page.getByRole('button', { name: 'File' }).click(); - const downloadPromise = page.waitForEvent('download'); - await page.locator('#menu-editor-file-export-li').click(); - const download = await downloadPromise; - await utils.compare_download_to_ref(download, "abc_download.ka"); - const downloaded_path = await download.path(); - - // close it, and reopen it - await page.getByRole('button', { name: 'File' }).click(); - await page.locator('#menu-editor-file-close-li').click(); - await page.getByRole('button', { name: 'File' }).click(); - const fileChooserPromise = page.waitForEvent('filechooser'); - await page.locator('#menu-editor-file-open-li').click(); - const fileChooser = await fileChooserPromise; - await fileChooser.setFiles(downloaded_path); - - // write other file and check contact map - await page.getByRole('button', { name: 'File' }).click(); - await page.locator('#menu-editor-file-new-li').click(); - await page.getByRole('textbox', { name: 'file name' }).click(); - await page.getByRole('textbox', { name: 'file name' }).fill('test.ka'); - await page.getByRole('textbox', { name: 'file name' }).press('Enter'); - await page.locator('.CodeMirror-scroll').click(); - await utils.input_in_editor_from_str(page, - `%agent: K(x) + // TODO: see if we can adapt part of this to electron? + if (!run_in_electron || RUN_DOWNLOADS_IN_ELECTRON) { + // download file + await page.getByRole('button', { name: 'File' }).click(); + const downloadPromise = page.waitForEvent('download'); + await page.locator('#menu-editor-file-export-li').click(); + const download = await downloadPromise; + await utils.compare_download_to_ref(download, "abc_download.ka"); + const downloaded_path = await download.path(); + + // close it, and reopen it + await page.getByRole('button', { name: 'File' }).click(); + await page.locator('#menu-editor-file-close-li').click(); + await page.getByRole('button', { name: 'File' }).click(); + const fileChooserPromise = page.waitForEvent('filechooser'); + await page.locator('#menu-editor-file-open-li').click(); + const fileChooser = await fileChooserPromise; + await fileChooser.setFiles(downloaded_path); + + // write other file and check contact map + await page.getByRole('button', { name: 'File' }).click(); + await page.locator('#menu-editor-file-new-li').click(); + await page.getByRole('textbox', { name: 'file name' }).click(); + await page.getByRole('textbox', { name: 'file name' }).fill('test.ka'); + await page.getByRole('textbox', { name: 'file name' }).press('Enter'); + await page.locator('.CodeMirror-scroll').click(); + await utils.input_in_editor_from_str(page, + `%agent: K(x) %agent: S(a b{u p} c{u p}) %init: 1000 K() %init: 1000 S() @@ -810,20 +841,22 @@ test.describe('projects_and_files', () => { %obs: 'S++' |S(b{p} c{p})| %mod: [true] do $TRACK 'S++' [true] ; ` - ); - await utils.wait_for_file_load(page, { timeout: 10000 }); - const contact_map = page.locator('#map-container'); - - const opts_screen = { maxDiffPixels: 150 } - await expect.soft(contact_map).toHaveScreenshot(opts_screen); - - // TODO: fix this flaky test: sometimes the graph doesn't show, bug? - // simulate and test screenshot - // await utils.set_pause_if(page, '[T] > 30'); - // await page.getByRole('button', { name: 'start' }).click(); - // await utils.wait_for_sim_stop(page, { timeout: 20000 }); - // await page.locator('#navplot').click(); - // await expect.soft(page.getByRole('img')).toHaveScreenshot(); + ); + await utils.wait_for_file_load(page, { timeout: 10000 }); + const contact_map = page.locator('#map-container'); + + const opts_screen = { maxDiffPixels: 150 } + await expect.soft(contact_map).toHaveScreenshot(opts_screen); + + // TODO: fix this flaky test: sometimes the graph doesn't show, bug? + // simulate and test screenshot + // await utils.set_pause_if(page, '[T] > 30'); + // await page.getByRole('button', { name: 'start' }).click(); + // await utils.wait_for_sim_stop(page, { timeout: 20000 }); + // await page.locator('#navplot').click(); + // await expect.soft(page.getByRole('img')).toHaveScreenshot(); + // + } }); }); diff --git a/tests/playwright/procedure.spec.ts-snapshots/Editor-tab-contact-map-accuracy-1-chromium-linux.png b/tests/playwright/procedure.spec.ts-snapshots/Editor-tab-contact-map-accuracy-1-chromium-linux.png index 9553b5cd1..b5c46e541 100644 Binary files a/tests/playwright/procedure.spec.ts-snapshots/Editor-tab-contact-map-accuracy-1-chromium-linux.png and b/tests/playwright/procedure.spec.ts-snapshots/Editor-tab-contact-map-accuracy-1-chromium-linux.png differ diff --git a/tests/playwright/procedure.spec.ts-snapshots/Editor-tab-contact-map-accuracy-1-firefox-linux.png b/tests/playwright/procedure.spec.ts-snapshots/Editor-tab-contact-map-accuracy-1-firefox-linux.png index 242008379..d22be5c5a 100644 Binary files a/tests/playwright/procedure.spec.ts-snapshots/Editor-tab-contact-map-accuracy-1-firefox-linux.png and b/tests/playwright/procedure.spec.ts-snapshots/Editor-tab-contact-map-accuracy-1-firefox-linux.png differ diff --git a/tests/playwright/procedure.spec.ts-snapshots/Editor-tab-contact-map-accuracy-2-chromium-linux.png b/tests/playwright/procedure.spec.ts-snapshots/Editor-tab-contact-map-accuracy-2-chromium-linux.png index 359809ee4..99a0a30d2 100644 Binary files a/tests/playwright/procedure.spec.ts-snapshots/Editor-tab-contact-map-accuracy-2-chromium-linux.png and b/tests/playwright/procedure.spec.ts-snapshots/Editor-tab-contact-map-accuracy-2-chromium-linux.png differ diff --git a/tests/playwright/procedure.spec.ts-snapshots/Editor-tab-contact-map-accuracy-2-firefox-linux.png b/tests/playwright/procedure.spec.ts-snapshots/Editor-tab-contact-map-accuracy-2-firefox-linux.png index 04875fa9f..26267ce7e 100644 Binary files a/tests/playwright/procedure.spec.ts-snapshots/Editor-tab-contact-map-accuracy-2-firefox-linux.png and b/tests/playwright/procedure.spec.ts-snapshots/Editor-tab-contact-map-accuracy-2-firefox-linux.png differ diff --git a/tests/playwright/project_electron_param.ts b/tests/playwright/project_electron_param.ts new file mode 100644 index 000000000..f5ddde55d --- /dev/null +++ b/tests/playwright/project_electron_param.ts @@ -0,0 +1,63 @@ +// Allows to define an electron project and use this param +// See https://playwright.dev/docs/test-parameterize#parameterized-projects +import { test as test_base } from '@playwright/test'; +import { _electron as electron } from 'playwright'; + +const electron_app_entry_point_path = 'build/Kappapp/resources/app/main.js'; +const electron_exe_path = 'build/Kappapp/kappapp'; + +export type TestOptions = { + run_in_electron: boolean; +} + +export const RUN_DOWNLOADS_IN_ELECTRON = false; + +// Note: for adding features https://playwright.dev/docs/api/class-test#test-extend +export const test = test_base.extend({ + // Define an option and provide a default value. + // We can later override it in the config. + run_in_electron: [false, { option: true }], + + // setup electron page if necessary + page: async ({ page, run_in_electron }, use) => { + if (run_in_electron) { + // TODO: check this: probably need to setup this as a fixture ? + console.info("Setting up electron app"); + const electronApp = await electron.launch({ + args: [ + "--ignore-gpu-blacklist", + // "--enable-logging", + electron_app_entry_point_path + ], + executablePath: electron_exe_path + }); + + // TODO: remove? + // Evaluation expression in the Electron context. + const appPath = await electronApp.evaluate(async ({ app }) => { + // This runs in the main Electron process, parameter here is always + // the result of the require('electron') in the main app script. + return app.getAppPath(); + }); + console.log("App path: ", appPath); + + const window = await electronApp.firstWindow(); + // Direct Electron console to Node terminal. + window.on('console', console.log); + + console.log(await window.title()); + + // TODO: add this to a fixture to close the app + // await electronApp.close(); + + await use(window); + window + } else { + // TODO: check if useful + await use(page); + page + } + }, +}); + +// TODO: fixture for launching electron if selected, and setting up page ? YES diff --git a/tests/playwright/refs/influences.json b/tests/playwright/refs/influences.json index e17688fec..cb8ab0bd7 100644 --- a/tests/playwright/refs/influences.json +++ b/tests/playwright/refs/influences.json @@ -1 +1 @@ -{"influence map":{"accuracy":"high","map":{"nodes":[{"rule":{"id":0,"label":"a.b","ast":"A(x[./1]), B(x[./1]) ","location":{"val":null,"loc":{"file":"abc.ka","bline":9,"bchr":0,"echr":37}},"direction":"direct","hidden":"false"}},{"rule":{"id":1,"label":"a..b","ast":"A(x[1/.]), B(x[1/.]) ","location":{"val":null,"loc":{"file":"abc.ka","bline":10,"bchr":0,"echr":39}},"direction":"direct","hidden":"false"}},{"rule":{"id":2,"label":"ab.c","ast":"A(x[_] c[./2]), C(x1{u}[./2]) ","location":{"val":null,"loc":{"file":"abc.ka","bline":11,"bchr":0,"echr":47}},"direction":"direct","hidden":"false"}},{"rule":{"id":3,"label":"mod x1","ast":"C(x1{u/p}[1/.]), A(c[1/.]) ","location":{"val":null,"loc":{"file":"abc.ka","bline":12,"bchr":0,"echr":47}},"direction":"direct","hidden":"false"}},{"rule":{"id":4,"label":"a.c","ast":"A(x[.] c[./1]), C(x1{p}[.] x2{u}[./1]) ","location":{"val":null,"loc":{"file":"abc.ka","bline":13,"bchr":0,"echr":55}},"direction":"direct","hidden":"false"}},{"rule":{"id":5,"label":"mod x2","ast":"A(x[.] c[1/.]), C(x1{p}[.] x2{u/p}[1/.]) ","location":{"val":null,"loc":{"file":"abc.ka","bline":14,"bchr":0,"echr":61}},"direction":"direct","hidden":"false"}},{"variable":{"id":0,"label":"on_rate","ast":"","location":{"val":null,"loc":{"file":"abc.ka","bline":17,"bchr":6,"echr":22}}}},{"variable":{"id":1,"label":"off_rate","ast":"","location":{"val":null,"loc":{"file":"abc.ka","bline":18,"bchr":6,"echr":20}}}},{"variable":{"id":2,"label":"mod_rate","ast":"","location":{"val":null,"loc":{"file":"abc.ka","bline":19,"bchr":6,"echr":18}}}},{"variable":{"id":3,"label":"AB","ast":"","location":{"val":null,"loc":{"file":"abc.ka","bline":20,"bchr":6,"echr":22}}}},{"variable":{"id":4,"label":"Cuu","ast":"","location":{"val":null,"loc":{"file":"abc.ka","bline":21,"bchr":6,"echr":28}}}},{"variable":{"id":5,"label":"Cpu","ast":"","location":{"val":null,"loc":{"file":"abc.ka","bline":22,"bchr":6,"echr":28}}}},{"variable":{"id":6,"label":"Cpp","ast":"","location":{"val":null,"loc":{"file":"abc.ka","bline":23,"bchr":6,"echr":28}}}}],"wake-up map":[{"source":{"rule":0},"target map":[{"target":{"rule":1},"location pair list":[{"RHS":{"direct":1},"LHS":{"direct":1}},{"RHS":{"direct":0},"LHS":{"direct":0}}]},{"target":{"rule":2},"location pair list":[{"RHS":{"direct":0},"LHS":{"direct":0}}]},{"target":{"variable":3},"location pair list":[{"RHS":{"direct":0},"LHS":{"direct":0}}]}]},{"source":{"rule":1},"target map":[{"target":{"rule":0},"location pair list":[{"RHS":{"direct":1},"LHS":{"direct":1}},{"RHS":{"direct":0},"LHS":{"direct":0}}]},{"target":{"rule":4},"location pair list":[{"RHS":{"direct":0},"LHS":{"direct":0}}]},{"target":{"rule":5},"location pair list":[{"RHS":{"direct":0},"LHS":{"direct":0}}]}]},{"source":{"rule":2},"target map":[{"target":{"rule":3},"location pair list":[{"RHS":{"direct":1},"LHS":{"direct":0}},{"RHS":{"direct":0},"LHS":{"direct":1}}]}]},{"source":{"rule":3},"target map":[{"target":{"rule":2},"location pair list":[{"RHS":{"direct":1},"LHS":{"direct":0}}]},{"target":{"rule":4},"location pair list":[{"RHS":{"direct":1},"LHS":{"direct":0}},{"RHS":{"direct":0},"LHS":{"direct":1}}]},{"target":{"rule":5},"location pair list":[{"RHS":{"direct":0},"LHS":{"direct":1}}]},{"target":{"variable":5},"location pair list":[{"RHS":{"direct":0},"LHS":{"direct":0}}]},{"target":{"variable":6},"location pair list":[{"RHS":{"direct":0},"LHS":{"direct":0}}]}]},{"source":{"rule":4},"target map":[{"target":{"rule":5},"location pair list":[{"RHS":{"direct":1},"LHS":{"direct":1}},{"RHS":{"direct":0},"LHS":{"direct":0}}]}]},{"source":{"rule":5},"target map":[{"target":{"rule":4},"location pair list":[{"RHS":{"direct":0},"LHS":{"direct":0}}]},{"target":{"variable":6},"location pair list":[{"RHS":{"direct":1},"LHS":{"direct":0}}]}]}],"inhibition map":[{"source":{"rule":0},"target map":[{"target":{"rule":4},"location pair list":[{"RHS":{"direct":0},"LHS":{"direct":0}}]},{"target":{"rule":5},"location pair list":[{"RHS":{"direct":0},"LHS":{"direct":0}}]}]},{"source":{"rule":1},"target map":[{"target":{"rule":2},"location pair list":[{"RHS":{"direct":0},"LHS":{"direct":0}}]},{"target":{"variable":3},"location pair list":[{"RHS":{"direct":0},"LHS":{"direct":0}}]}]},{"source":{"rule":3},"target map":[{"target":{"variable":4},"location pair list":[{"RHS":{"direct":0},"LHS":{"direct":0}}]}]},{"source":{"rule":5},"target map":[{"target":{"variable":5},"location pair list":[{"RHS":{"direct":1},"LHS":{"direct":0}}]}]}]}}} \ No newline at end of file +{"influence map":{"accuracy":"high","map":{"nodes":[{"rule":{"id":0,"label":"a.b","ast":"A(x[./1]), B(x[./1]) ","location":{"val":null,"loc":{"file":"model.ka","bline":9,"bchr":0,"echr":37}},"direction":"direct","hidden":"false"}},{"rule":{"id":1,"label":"a..b","ast":"A(x[1/.]), B(x[1/.]) ","location":{"val":null,"loc":{"file":"model.ka","bline":10,"bchr":0,"echr":39}},"direction":"direct","hidden":"false"}},{"rule":{"id":2,"label":"ab.c","ast":"A(x[_] c[./2]), C(x1{u}[./2]) ","location":{"val":null,"loc":{"file":"model.ka","bline":11,"bchr":0,"echr":47}},"direction":"direct","hidden":"false"}},{"rule":{"id":3,"label":"mod x1","ast":"C(x1{u/p}[1/.]), A(c[1/.]) ","location":{"val":null,"loc":{"file":"model.ka","bline":12,"bchr":0,"echr":47}},"direction":"direct","hidden":"false"}},{"rule":{"id":4,"label":"a.c","ast":"A(x[.] c[./1]), C(x1{p}[.] x2{u}[./1]) ","location":{"val":null,"loc":{"file":"model.ka","bline":13,"bchr":0,"echr":55}},"direction":"direct","hidden":"false"}},{"rule":{"id":5,"label":"mod x2","ast":"A(x[.] c[1/.]), C(x1{p}[.] x2{u/p}[1/.]) ","location":{"val":null,"loc":{"file":"model.ka","bline":14,"bchr":0,"echr":61}},"direction":"direct","hidden":"false"}},{"variable":{"id":0,"label":"on_rate","ast":"","location":{"val":null,"loc":{"file":"model.ka","bline":17,"bchr":6,"echr":22}}}},{"variable":{"id":1,"label":"off_rate","ast":"","location":{"val":null,"loc":{"file":"model.ka","bline":18,"bchr":6,"echr":20}}}},{"variable":{"id":2,"label":"mod_rate","ast":"","location":{"val":null,"loc":{"file":"model.ka","bline":19,"bchr":6,"echr":18}}}},{"variable":{"id":3,"label":"AB","ast":"","location":{"val":null,"loc":{"file":"model.ka","bline":20,"bchr":6,"echr":22}}}},{"variable":{"id":4,"label":"Cuu","ast":"","location":{"val":null,"loc":{"file":"model.ka","bline":21,"bchr":6,"echr":28}}}},{"variable":{"id":5,"label":"Cpu","ast":"","location":{"val":null,"loc":{"file":"model.ka","bline":22,"bchr":6,"echr":28}}}},{"variable":{"id":6,"label":"Cpp","ast":"","location":{"val":null,"loc":{"file":"model.ka","bline":23,"bchr":6,"echr":28}}}}],"wake-up map":[{"source":{"rule":0},"target map":[{"target":{"rule":1},"location pair list":[{"RHS":{"direct":1},"LHS":{"direct":1}},{"RHS":{"direct":0},"LHS":{"direct":0}}]},{"target":{"rule":2},"location pair list":[{"RHS":{"direct":0},"LHS":{"direct":0}}]},{"target":{"variable":3},"location pair list":[{"RHS":{"direct":0},"LHS":{"direct":0}}]}]},{"source":{"rule":1},"target map":[{"target":{"rule":0},"location pair list":[{"RHS":{"direct":1},"LHS":{"direct":1}},{"RHS":{"direct":0},"LHS":{"direct":0}}]},{"target":{"rule":4},"location pair list":[{"RHS":{"direct":0},"LHS":{"direct":0}}]},{"target":{"rule":5},"location pair list":[{"RHS":{"direct":0},"LHS":{"direct":0}}]}]},{"source":{"rule":2},"target map":[{"target":{"rule":3},"location pair list":[{"RHS":{"direct":1},"LHS":{"direct":0}},{"RHS":{"direct":0},"LHS":{"direct":1}}]}]},{"source":{"rule":3},"target map":[{"target":{"rule":2},"location pair list":[{"RHS":{"direct":1},"LHS":{"direct":0}}]},{"target":{"rule":4},"location pair list":[{"RHS":{"direct":1},"LHS":{"direct":0}},{"RHS":{"direct":0},"LHS":{"direct":1}}]},{"target":{"rule":5},"location pair list":[{"RHS":{"direct":0},"LHS":{"direct":1}}]},{"target":{"variable":5},"location pair list":[{"RHS":{"direct":0},"LHS":{"direct":0}}]},{"target":{"variable":6},"location pair list":[{"RHS":{"direct":0},"LHS":{"direct":0}}]}]},{"source":{"rule":4},"target map":[{"target":{"rule":5},"location pair list":[{"RHS":{"direct":1},"LHS":{"direct":1}},{"RHS":{"direct":0},"LHS":{"direct":0}}]}]},{"source":{"rule":5},"target map":[{"target":{"rule":4},"location pair list":[{"RHS":{"direct":0},"LHS":{"direct":0}}]},{"target":{"variable":6},"location pair list":[{"RHS":{"direct":1},"LHS":{"direct":0}}]}]}],"inhibition map":[{"source":{"rule":0},"target map":[{"target":{"rule":4},"location pair list":[{"RHS":{"direct":0},"LHS":{"direct":0}}]},{"target":{"rule":5},"location pair list":[{"RHS":{"direct":0},"LHS":{"direct":0}}]}]},{"source":{"rule":1},"target map":[{"target":{"rule":2},"location pair list":[{"RHS":{"direct":0},"LHS":{"direct":0}}]},{"target":{"variable":3},"location pair list":[{"RHS":{"direct":0},"LHS":{"direct":0}}]}]},{"source":{"rule":3},"target map":[{"target":{"variable":4},"location pair list":[{"RHS":{"direct":0},"LHS":{"direct":0}}]}]},{"source":{"rule":5},"target map":[{"target":{"variable":5},"location pair list":[{"RHS":{"direct":1},"LHS":{"direct":0}}]}]}]}}} \ No newline at end of file diff --git a/tests/playwright/webapp_utils.ts b/tests/playwright/webapp_utils.ts index 9e0e7d929..22dd89445 100644 --- a/tests/playwright/webapp_utils.ts +++ b/tests/playwright/webapp_utils.ts @@ -101,10 +101,28 @@ export async function input_in_editor_from_url(page: Page, url_protocol_relative await input_in_editor_from_str(page, model); } -export async function open_app_with_model(page: Page, url_protocol_relative: string, paste_in_editor: boolean = false, timeout: number = 10000) { - if (paste_in_editor) { - // download the file and paste it in the editor +export async function open_app_with_model_from_text(page: Page, model_text: string, run_in_electron: boolean, timeout: number = 10000) { + if (!run_in_electron) { + // load the app if not in electron + // if in electron, the page is already loaded, and we just need to enter the file in the editor await page.goto(url); + } + await wait_for_project_ready_status(page); + await input_in_editor_from_str(page, model_text); + + // Note: if fails in input_in_editor_from_str, it won't wait for second timeout as expect is not expect.soft + await wait_for_file_load(page, { timeout: timeout }); +} + +// TODO: paste_in_editor is now always true, as default is true +export async function open_app_with_model(page: Page, url_protocol_relative: string, run_in_electron: boolean, paste_in_editor: boolean = true, timeout: number = 10000) { + if (paste_in_editor || run_in_electron) { + // download the file and paste it in the editor + if (!run_in_electron) { + // load the app if not in electron + // if in electron, the page is already loaded, and we just need to enter the file in the editor + await page.goto(url); + } await wait_for_project_ready_status(page); await input_in_editor_from_url(page, url_protocol_relative); } else { @@ -116,6 +134,7 @@ export async function open_app_with_model(page: Page, url_protocol_relative: str } export function get_error_field(page: Page) { + // Note: alternative emplacement, showing different info, in case it's relevant // return page.locator('#configuration_error_div'); return page.locator('#configuration_alert_div'); }