Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
989bff9
feat(logging-view,simple): add orders to charts
JavierRibaldelRio Oct 7, 2026
ddb9bde
fix(logging-view): overlaping title and order box
JavierRibaldelRio Oct 9, 2026
9795ce9
fix(logging-view, simple): put orders to the right menu and ad board …
JavierRibaldelRio Oct 9, 2026
292d2e0
feat(logging-view): add menu to see orders only
JavierRibaldelRio Oct 9, 2026
8a5b7e4
fix(logging-view, simple): dark mode color scheme
JavierRibaldelRio Oct 9, 2026
21b6afb
feat(logging-view): startwith advanced mode
JavierRibaldelRio Oct 9, 2026
a91eca7
feat(logging-view, normal): general enhancements
JavierRibaldelRio Oct 10, 2026
b133691
Merge branch 'develop' into frontend/logging-view-enhancements
JavierRibaldelRio Nov 8, 2026
e9c6e21
fix(logging-view): dependency mess
JavierRibaldelRio Nov 8, 2026
2946268
fix(logging-view): remove uneded header
JavierRibaldelRio Nov 8, 2026
e81ca1e
fix(logging-view): unify two bars
JavierRibaldelRio Nov 8, 2026
64cc2f2
feat(logging-view): tabs
JavierRibaldelRio Nov 8, 2026
f593cd1
fix(logging-view): replace h11 logo by generic logo
JavierRibaldelRio Nov 8, 2026
add47b1
chore(e2e, logging-view): update tests
JavierRibaldelRio Nov 8, 2026
4daa5df
fix(e2e): seed the About credits the suite can no longer find
JavierRibaldelRio Nov 8, 2026
dae3fd4
feat(logging-view, normal): add ecahtr normal integration!
JavierRibaldelRio Nov 9, 2026
20e69e8
fix(logging-iew): hide and show stats button
JavierRibaldelRio Nov 9, 2026
1219c08
feat(logging-view, normal): allow right cclick
JavierRibaldelRio Nov 9, 2026
2dc0fb7
feat(logging-view, normal): add orders
JavierRibaldelRio Nov 9, 2026
80b4b96
feat(frontend, frontend-kit): add icons
JavierRibaldelRio Nov 11, 2026
6d611b2
feat(logging-view): history
JavierRibaldelRio Nov 11, 2026
578fa5f
feat(logging-view, normal): enhance stats
JavierRibaldelRio Nov 11, 2026
defe2c7
fix:(logging-view, sidebar ): keep closed sesion when opened
JavierRibaldelRio Nov 11, 2026
b316572
markers
JavierRibaldelRio Nov 11, 2026
dce8f26
fix(logging-view, normal): layout bar add micro seconds to select of …
JavierRibaldelRio Nov 11, 2026
83dc7bb
feat()
JavierRibaldelRio Nov 13, 2026
0a00e1b
fix(logging-view, normal): keep charts unlinked when a plot is added
JavierRibaldelRio Nov 13, 2026
08941b1
fix(logging-view, normal): layout bar disable times when charts are u…
JavierRibaldelRio Nov 13, 2026
ba5e83b
feat(logging-view, normal): plot menu submenus, ctrl+drag zoom and fi…
JavierRibaldelRio Nov 13, 2026
589e73f
feat(logging-view, normal): A/B measuring cursors
JavierRibaldelRio Nov 13, 2026
2b7962d
feat(logging-view, normal): keyboard shortcuts and shortcuts help
JavierRibaldelRio Nov 13, 2026
23475b5
feat(logging-view, normal): dashboards restore on session reopen and …
JavierRibaldelRio Nov 13, 2026
27a7bd8
fix(logging-view, normal): shortcuts keep arrows for tabs and footer …
JavierRibaldelRio Nov 14, 2026
c03dc3c
feat(logging-view, normal): drag cursors and step through orders and …
JavierRibaldelRio Nov 14, 2026
94cfd07
test(logging-view): raise e2e timeout to 60 s for loaded machines
JavierRibaldelRio Nov 14, 2026
11b68a0
feat(logging-view, normal): export statistics, cursors and plots as C…
JavierRibaldelRio Nov 14, 2026
c1e2ccf
feat(logging-view, normal): double-click a measurement and search to …
JavierRibaldelRio Nov 14, 2026
03dda80
feat(logging-view, normal): undo and redo dashboard changes
JavierRibaldelRio Nov 14, 2026
79dc444
feat(logging-view, normal): dashboard templates
JavierRibaldelRio Nov 14, 2026
dc1683d
feat(logging-view, normal): X-Y plots of one signal against another
JavierRibaldelRio Nov 14, 2026
3f9f320
fix(logging-view, normal): deleting a template no longer applies it
JavierRibaldelRio Nov 14, 2026
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
22 changes: 22 additions & 0 deletions e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ Tests are split into two Playwright projects:
| `ui` | `tests/ui/` | UI tests — launch the Electron app and interact with the UI |
| `integration` | `tests/integration/` | Real renderer/main IPC, Go HTTP catalogs, WebSocket subscriptions and profile isolation |

Logging View also has a standalone browser suite in `tests/logging-view/`, using
`playwright.logging.config.ts`. Run `pnpm --dir e2e test:logging-view` from the
repository root. It starts Vite on port 9007 and verifies dashboard tabs, independent
signal/layout/time settings, keyboard navigation, and session resets without Electron
or a backend binary.
The suite also covers the tab settings popover, colors, drag and keyboard reordering,
and lock protection for tab metadata, plots, sidebar selection and time controls.
Compact controls in the tab bar and the expandable full controls are checked for
state preservation, lock protection and narrow layouts.

---

## UI Tests
Expand Down Expand Up @@ -57,18 +67,30 @@ vehicle commands. The WebSocket integration test only subscribes to connection s
| `pnpm test:fast` | Run all tests without rebuilding (assumes already built) |
| `pnpm test:fast:ui` | Run `tests/ui` without rebuilding |
| `pnpm test:fast:integration` | Run integration tests without rebuilding |
| `pnpm test:logging-view` | Run Logging View dashboard tests against a local Vite server |
| `pnpm report` | Open the last Playwright HTML report |

The renderer build does not compile Go or package Electron. It copies each
frontend once, preserving Testing's E2E build rather than rebuilding it in normal mode.
Use `:fast` variants when iterating against already-built renderers.

`pnpm build` also seeds the About credits. The About screen reads a snapshot of
contributor photographs and per-person commit counts that `generateCredits.js`
fetches from the GitHub API when the app is packaged; it is gitignored, so a
fresh clone — every CI run — has neither it nor the avatars beside it, and
`tests/ui/about.test.ts` would find an empty contributor list. The build writes
a fixed, offline fixture through `scripts/about-credits.mjs` when no usable
snapshot is on disk, and leaves a generated one untouched, so a developer's own
build stays what the About tests exercise locally. The suite never calls GitHub:
the About tests assert that the screen is entirely network-free.

---

## Requirements

- A platform-appropriate, prebuilt backend binary must exist in `electron-app/binaries/`. CI builds it before running the suite.
- `testing-view` must be built in e2e mode (`build:e2e`), also handled automatically.
- About credits need no network or GitHub token: `pnpm build` seeds an offline fixture when nothing has generated the snapshot.
- Workers are set to `1` — Electron tests must run serially since only one app instance can run at a time.

CI builds all four tested views and runs both projects. `E2E_REQUIRED_VIEWS=testing,competition,logging,adj` makes missing CI build artifacts a failure instead of silently skipping cross-view tests. Fast local runs may still skip optional-view cases when testing a reduced build.
Expand Down
1 change: 1 addition & 0 deletions e2e/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"test:fast": "playwright test",
"test:fast:ui": "playwright test tests/ui",
"test:fast:integration": "playwright test --project=integration",
"test:logging-view": "playwright test --config playwright.logging.config.ts",
"report": "playwright show-report"
},
"devDependencies": {
Expand Down
19 changes: 19 additions & 0 deletions e2e/playwright.logging.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { defineConfig } from "@playwright/test";

export default defineConfig({
testDir: "./tests/logging-view",
timeout: 60_000,
workers: 1,
reporter: "list",
use: {
baseURL: "http://127.0.0.1:9007",
viewport: { width: 1440, height: 900 },
trace: "retain-on-failure",
},
webServer: {
command:
"pnpm --dir ../frontend/logging-view dev --host 127.0.0.1 --port 9007 --strictPort",
url: "http://127.0.0.1:9007",
reuseExistingServer: !process.env.CI,
},
});
185 changes: 185 additions & 0 deletions e2e/scripts/about-credits.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
/**
* The About screen reads electron-app/src/about/credits.snapshot.json and the
* avatars next to it. Both are generated from the GitHub API at build time and
* deliberately not committed (they are contributor photographs and per-person
* commit counts, in a public repository), so a fresh clone — every CI run —
* has neither, About lists nobody and tests/ui/about.test.ts cannot find a
* single contributor.
*
* The suite must not fetch them either: the About tests assert that the screen
* is entirely network-free, and making every pull request depend on the GitHub
* commits API would be slow and flaky. So seed a fixed, offline snapshot plus
* matching images, and leave a real generated snapshot alone when there is one,
* so a developer's own build keeps being what the tests exercise locally.
*/
import { deflateSync } from "node:zlib";
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";

const ELECTRON_APP = fileURLToPath(
new URL("../../electron-app/", import.meta.url),
);
const SNAPSHOT = path.join(ELECTRON_APP, "src/about/credits.snapshot.json");
const AVATARS = path.join(ELECTRON_APP, "renderer/about/avatars");
// The renderer only builds an <img> for a path shaped like this one.
const AVATAR_URL = /^\.\/avatars\/[a-zA-Z0-9-]+\.(png|jpg|webp)$/;

// Ids far outside GitHub's range for this repository's members, so the fixture
// images can never be mistaken for, or overwrite, a real contributor's photo.
// Descending, distinct counts: the first test asserts the list is sorted and
// the second one opens the top member's profile, which needs a login.
const FIXTURE = [
{
id: "900000001",
login: "e2e-ada",
contributions: 128,
colour: [64, 110, 235],
},
{
id: "900000002",
login: "e2e-grace",
contributions: 57,
colour: [217, 72, 140],
},
{
id: "900000003",
login: "e2e-linus",
contributions: 9,
colour: [48, 168, 122],
},
];

const CRC_TABLE = Uint32Array.from({ length: 256 }, (_, index) => {
let value = index;
for (let bit = 0; bit < 8; bit++)
value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
return value >>> 0;
});

function crc32(buffer) {
let crc = 0xffffffff;
for (const byte of buffer) crc = CRC_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8);
return (crc ^ 0xffffffff) >>> 0;
}

function chunk(type, data) {
const head = Buffer.alloc(8);
head.writeUInt32BE(data.length, 0);
head.write(type, 4, "ascii");
const tail = Buffer.alloc(4);
tail.writeUInt32BE(crc32(Buffer.concat([head.subarray(4), data])), 0);
return Buffer.concat([head, data, tail]);
}

/**
* Encodes a solid square as a real PNG. The test requires each avatar to
* decode in the renderer (`naturalWidth > 0`), so these are complete images
* rather than placeholder bytes, and no binary fixture needs committing.
* @param {number} size - Side length in pixels.
* @param {[number, number, number]} colour - 8-bit RGB fill.
* @returns {Buffer}
*/
function solidPng(size, colour) {
const stride = size * 3 + 1;
const raw = Buffer.alloc(stride * size);
for (let row = 0; row < size; row++) {
raw[row * stride] = 0; // Filter type 0: no filtering.
for (let column = 0; column < size; column++) {
const pixel = row * stride + 1 + column * 3;
raw[pixel] = colour[0];
raw[pixel + 1] = colour[1];
raw[pixel + 2] = colour[2];
}
}
const header = Buffer.alloc(13);
header.writeUInt32BE(size, 0);
header.writeUInt32BE(size, 4);
header[8] = 8; // Bit depth.
header[9] = 2; // Colour type: truecolour RGB.
return Buffer.concat([
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
chunk("IHDR", header),
chunk("IDAT", deflateSync(raw, { level: 9 })),
chunk("IEND", Buffer.alloc(0)),
]);
}

/**
* Reports whether the snapshot on disk can carry the About tests: it must
* list somebody, be counted the way getAboutCredits() demands, name at least
* one GitHub profile to open, and reference only images that are still there,
* because a missing file leaves an <img> that never loads.
* @returns {Promise<boolean>}
*/
async function hasUsableCredits() {
let snapshot;
try {
snapshot = JSON.parse(await readFile(SNAPSHOT, "utf8"));
} catch {
return false;
}
const people = snapshot?.contributors;
if (snapshot?.counting !== "non-merge" || !Array.isArray(people))
return false;
const listed = people.filter((person) => person.type !== "Bot");
if (!listed.length || !listed.some((person) => person.login)) return false;
const images = listed
.map((person) => person.avatarUrl)
.filter((url) => typeof url === "string" && url);
if (!images.length || !images.every((url) => AVATAR_URL.test(url)))
return false;
const present = await Promise.all(
images.map((url) =>
access(path.join(AVATARS, path.basename(url))).then(
() => true,
() => false,
),
),
);
return present.every(Boolean);
}

/**
* Writes the offline About fixture unless a usable generated snapshot is
* already on disk.
* @returns {Promise<"fixture" | "generated">} Which credits the tests will read.
*/
export async function seedAboutCredits() {
if (await hasUsableCredits()) return "generated";
await mkdir(AVATARS, { recursive: true });
await mkdir(path.dirname(SNAPSHOT), { recursive: true });
await Promise.all(
FIXTURE.map((person) =>
writeFile(
path.join(AVATARS, `${person.id}.png`),
solidPng(96, person.colour),
),
),
);
await writeFile(
SNAPSHOT,
`${JSON.stringify(
{
schemaVersion: 3,
repository: "Hyperloop-UPV/software",
counting: "non-merge",
// No revision, so the next real build refetches instead of reusing this.
headSha: null,
fetchedAt: "2026-01-02T03:04:05.000Z",
contributors: FIXTURE.map(({ id, login, contributions }) => ({
id,
login,
name: login,
contributions,
avatarUrl: `./avatars/${id}.png`,
type: "User",
})),
},
null,
2,
)}\n`,
"utf8",
);
return "fixture";
}
9 changes: 9 additions & 0 deletions e2e/scripts/build-renderers.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { execFileSync } from "node:child_process";
import { cpSync, existsSync, mkdirSync, rmSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { seedAboutCredits } from "./about-credits.mjs";

const root = fileURLToPath(new URL("../../", import.meta.url));
const pnpmCli = process.env.npm_execpath;
Expand Down Expand Up @@ -32,3 +33,11 @@ for (const view of ["testing", "competition", "logging", "adj"]) {
mkdirSync(destination, { recursive: true });
cpSync(source, destination, { recursive: true });
}

// About reads a build-time snapshot that a fresh clone does not have.
const credits = await seedAboutCredits();
console.log(
credits === "fixture"
? "About credits: wrote the offline e2e fixture."
: "About credits: keeping the generated snapshot already on disk.",
);
Loading
Loading