Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .bazelrc
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ build --strategy=CopyFile=local
build --strategy=CopyToDirectory=local
build --strategy=CopyDirectory=local
build --strategy=NpmLifecycleHook=local
test --strategy=TestRunner=local

## JS

Expand Down
2 changes: 0 additions & 2 deletions BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,6 @@ ENGINE_DIST_GLOB_EXCLUDES = [
"**/.metals/**",
"**/.enso/**",
"**/.enso-sources*",
# Generated/downloaded resources should not be treated as inputs.
"lib/scala/pkg/src/main/resources/**",
# Binary test fixtures (often ignored) should not affect distribution builds.
"lib/scala/runtime-version-manager/src/test/resources/**",
]
Expand Down
20 changes: 20 additions & 0 deletions app/electron-client/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ load("@aspect_rules_js//npm:defs.bzl", "npm_package")
load("@aspect_rules_ts//ts:defs.bzl", "ts_config", "ts_project")
load("@npm//:defs.bzl", "npm_link_all_packages", "npm_link_targets")
load("@npm//app/electron-client:electron-builder/package_json.bzl", electron_builder_bin = "bin")
load("@npm//app/electron-client:playwright/package_json.bzl", playwright_bin = "bin")
load("@rules_shell//shell:sh_binary.bzl", "sh_binary")
load("//:bazel_scripts/ts_config.bzl", "write_tsconfig")
load("//internal:stampFiles.bzl", "stamp_files")
Expand Down Expand Up @@ -233,3 +234,22 @@ write_source_files(
suggested_update_target = "//:write_all",
visibility = ["//visibility:public"],
)

playwright_bin.playwright_test(
name = "test",
data = npm_link_targets() + glob(["tests/*.ts"]) + [
"playwright/.auth/user.json",
":dist",
"playwright.config.ts",
"selective-symlink-loader.mjs",
"selective-symlink-loader-hooks.mjs",
],
env = {
"ENSO_EXEC_PATH": "$(rootpath :dist)"
},
args = ["test", "--config=$(rootpath playwright.config.ts)"],
visibility = ["//visibility:public"],
log_level = "debug",
node_options = ["--import=./$(rootpath selective-symlink-loader.mjs)"],
tags = ["local"],
)
3 changes: 1 addition & 2 deletions app/electron-client/electron-builder-config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,7 @@ async function patchAppImage(context) {
set -u

SCRIPT_DIR="$( cd "$( dirname "\${BASH_SOURCE[0]}" )" && pwd )"
# TODO[ib]: check if this is even needed with native builds on Linux
export PATH="$PATH:$SCRIPT_DIR/resources/enso/runtime/${runtimeDirName}/bin"
export PATH="$SCRIPT_DIR/resources/enso/runtime/${runtimeDirName}/bin:$PATH"
exec "$SCRIPT_DIR/${executableName}.bin" --no-sandbox "$@"
`
try {
Expand Down
7 changes: 7 additions & 0 deletions app/electron-client/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,19 @@ export default defineConfig({
workers: 1,
timeout: 180000,
reportSlowTests: { max: 5, threshold: 60000 },
reporter: [
['list'],
['html', { outputFolder: 'html-report', open: 'never' }],
['json', { outputFile: 'report.json' }],
],
expect: {
timeout: 30000,
toHaveScreenshot: { threshold: 0 },
},
use: {
actionTimeout: 5000,
viewport: { width: 1780, height: 1000 },
screenshot: process.env.ENSO_PW_SCREENSHOTS ? 'only-on-failure' : 'off',
video: process.env.ENSO_PW_VIDEO ? 'retain-on-failure' : 'off',
},
})
62 changes: 62 additions & 0 deletions app/electron-client/selective-symlink-loader-hooks.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* ESM loader hooks for Bazel + Playwright integration.
* This file is registered via module.register() from selective-symlink-loader.mjs
*/

import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'

// Get the runfiles node_modules path from the main loader
// This is set by the main loader before registering this hooks file
let runfilesNodeModules = null

export function initialize(data) {
// Get runfiles path from environment since globalThis isn't shared
const runfilesDir = process.env.JS_BINARY__RUNFILES
runfilesNodeModules = runfilesDir ? path.join(runfilesDir, '_main', 'node_modules') : null
}

/**
* Helper function to rewrite bin/node_modules paths to runfiles
*/
function rewriteToRunfiles(resolved) {
if (!runfilesNodeModules) {
return null
}

const normalized = path.normalize(resolved)
const marker = `${path.sep}bin${path.sep}node_modules${path.sep}`
const markerIndex = normalized.indexOf(marker)
if (markerIndex === -1) {
return null
}

const relativePath = normalized.slice(markerIndex + marker.length)
const rewritten = path.join(runfilesNodeModules, relativePath)

try {
fs.accessSync(rewritten)
return rewritten
} catch {
return null
}
}

/**
* ESM resolve hook - intercepts module resolution
*/
export async function resolve(specifier, context, nextResolve) {
const result = await nextResolve(specifier, context)

if (result.url && result.url.startsWith('file://')) {
const filePath = fileURLToPath(result.url)
const rewritten = rewriteToRunfiles(filePath)
if (rewritten) {
const rewrittenUrl = pathToFileURL(rewritten).href
return { ...result, url: rewrittenUrl }
}
}

return result
}
20 changes: 20 additions & 0 deletions app/electron-client/selective-symlink-loader.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* Custom ESM module loader for Playwright + Bazel integration.
*
* Problem: Bazel creates two node_modules paths:
* - bin/node_modules/ (config file playwright.config.ts resolves here via symlink)
* - runfiles/_main/node_modules/ (test files resolve here)
*
* Node.js treats same module at different paths as separate instances,
* triggering Playwright's dual-instance detection.
*
* Solution: Register ESM hooks to rewrite bin/node_modules paths to
* runfiles/_main/node_modules, ensuring consistent module identity.
*
* This file is loaded via --import flag in //app/electron-client:test Bazel rule.
*/

import { register } from 'node:module'

// Register ESM loader hooks from a separate file
register('./selective-symlink-loader-hooks.mjs', import.meta.url)
4 changes: 4 additions & 0 deletions app/electron-client/src/globals.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ declare global {
readonly ENSO_TEST_PROJECTS_DIR?: string
readonly ENSO_TEST_APP_ARGS?: string
readonly ENSO_TEST_USER?: string
readonly ENSO_TEST_PASS?: string
readonly ENSO_TEST_PARTITION?: string
readonly ENSO_EXEC_PATH?: string
readonly JS_BINARY__RUNFILES?: string
ENSO_TEST_EXEC_PATH?: string

// === Electron watch script variables ===
Expand Down
3 changes: 2 additions & 1 deletion app/electron-client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,12 +346,13 @@ class App {

const guiConfig = await loadGuiConfig()
const encodedGuiConfig = Buffer.from(JSON.stringify(guiConfig), 'utf8').toString('base64')
const testPartition = process.env.ENSO_TEST ? (process.env.ENSO_TEST_PARTITION ?? 'test') : null
const webPreferences: WebPreferences = {
preload: joinPath(appPath(this.electron), 'preload.mjs'),
sandbox: true,
spellcheck: false,
additionalArguments: [`--enso-gui-config=${encodedGuiConfig}`],
...(process.env.ENSO_TEST ? { partition: 'test' } : {}),
...(testPartition ? { partition: testPartition } : {}),
}
const windowPreferences: BrowserWindowConstructorOptions = {
webPreferences,
Expand Down
6 changes: 4 additions & 2 deletions app/electron-client/tests/cloudWorkflow.spec.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
/** @file A series of tests designed for testing GUI behavior in Cloud. */

import { expect } from 'playwright/test'
import { test as base, expect } from 'playwright/test'
import {
closeWelcome,
createNewProject,
electronFixtures,
getNewestProject,
loginAsTestUser,
test,
} from './electronTest'

const test = base.extend(electronFixtures)

// A test controlling if project session logs aren't empty. Currently skipped due to unconsistency of session logs
test.skip('Session logs', async ({ page }) => {
await loginAsTestUser(page)
Expand Down
Loading
Loading