Skip to content

Commit e990e1e

Browse files
authored
Timeline resource streaming (#1929)
2 parents d6e6414 + e8b2f38 commit e990e1e

27 files changed

Lines changed: 1805 additions & 303 deletions

docs/TESTING.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ If this is your first time running the tests you may need to install the Playwri
3030
npx playwright install
3131
```
3232

33+
#### Auth failures
34+
35+
If several tests are failing due to auth issues, you may need to clear your Playwright cache first:
36+
37+
```sh
38+
npm run test:e2e:clear-cache
39+
```
40+
3341
#### New backend service:
3442

3543
If a new backend service has been added to PlanDev, make sure to update the [docker-compose-test.yml](../docker-compose-test.yml) in order for the CI to be able to spin up the backend correctly.

e2e-tests/fixtures/Plan.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,7 @@ export class Plan {
467467
async goto(planId = this.plans.planId) {
468468
await this.page.goto(`/plans/${planId}`, { waitUntil: 'load' });
469469
await this.page.waitForURL(`/plans/${planId}`, { waitUntil: 'load' });
470+
await this.planTitle.waitFor({ state: 'visible' });
470471
await this.waitForTimelineLoading();
471472
}
472473

e2e-tests/tests/model-derivation-group.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ test.afterAll(async () => {
8989

9090
test.describe.serial('Model Derivation Group Linking', () => {
9191
test('Derivation groups can be linked to a model and show in plan', async ({ baseURL }) => {
92+
test.setTimeout(60 * 1000);
9293
// check the current plan...
9394
await originalPlan.goto();
9495
await setup.page.waitForURL(`${baseURL}/plans/${originalPlanId}`, { timeout: 3000 });
@@ -115,14 +116,15 @@ test.describe.serial('Model Derivation Group Linking', () => {
115116
// now, create a new plan
116117
await setup.plans.goto();
117118
await setup.page.waitForURL(`${baseURL}/plans`, { timeout: 3000 });
118-
await setup.plans.createPlan('secondPlan', setup.modelName);
119+
const newPlanName = 'secondPlan';
120+
await setup.plans.createPlan(newPlanName, setup.modelName);
119121
newPlan = new Plan(
120122
setup.page,
121123
setup.plans,
122124
setup.constraints,
123125
setup.schedulingGoals,
124126
setup.schedulingConditions,
125-
setup.planName,
127+
newPlanName,
126128
);
127129
newPlanId = Number(setup.plans.planId);
128130

e2e-tests/tests/plan-external-source.test.ts

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import test, { expect } from '@playwright/test';
22
import { ExternalSources } from '../fixtures/ExternalSources.js';
33
import { PanelNames, Plan } from '../fixtures/Plan.js';
4+
import { anyCanvasHasContent } from '../utilities/canvas.js';
45
import {
56
cleanupApiResources,
67
closeBrowserResources,
@@ -157,22 +158,7 @@ test.describe.serial('Plan External Sources', () => {
157158
});
158159

159160
test('Zero-duration events are properly drawn in the timeline', async () => {
160-
// Get the current timeline canvas' pixels - use a set to just determine that non-0 RGB values exist
161-
const doPixelsExist: boolean = await setup.page.evaluate(() => {
162-
const canvas = document.querySelector('canvas');
163-
if (canvas !== null && canvas !== undefined) {
164-
const context = canvas.getContext('2d');
165-
if (context !== null && context !== undefined) {
166-
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
167-
const pixelData = Array.from(imageData.data);
168-
return pixelData.length > 0 ? true : false;
169-
// Assert that the number of unique RGB pixel values for the canvas is more than 0 (i.e., not empty)
170-
}
171-
}
172-
return false;
173-
});
174-
175-
expect(doPixelsExist).toBeTruthy();
161+
expect(await anyCanvasHasContent(setup.page, '[data-component-name="TimelinePanel"] canvas')).toBeTruthy();
176162
});
177163

178164
test('Linked derivation groups should be expandable in panel', async () => {

e2e-tests/tests/simulation.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import test, { expect } from '@playwright/test';
22
import { Status } from '../../src/enums/status.js';
33
import { PanelNames } from '../fixtures/Plan.js';
44
import { setupTest, teardownTest, type FullSetupResult } from '../utilities/api.js';
5+
import { anyCanvasHasContent } from '../utilities/canvas.js';
56

67
let setup: FullSetupResult;
78

@@ -61,9 +62,31 @@ test.describe.serial('Simulation', async () => {
6162
await setup.plan.showPanel(PanelNames.SIMULATION, true);
6263
});
6364

65+
// Smoke test for the windowed-pull pipeline: indicator settles cleanly and
66+
// canvases render non-transparent content (catches the "blank plot" bug an
67+
// indicator-only check would miss).
68+
test(`Streaming pipeline: indicator settles + canvases render across two re-sims`, async () => {
69+
const timelineErrorIndicator = setup.plan.page.getByRole('status', { name: 'Timeline data error' });
70+
const timelineLoadingIndicator = setup.plan.page.getByRole('status', { name: 'Timeline loading' });
71+
const timelineCanvasContent = () => anyCanvasHasContent(setup.page, '[data-component-name="TimelinePanel"] canvas');
72+
73+
await setup.plan.reRunSimulation();
74+
await expect(timelineErrorIndicator).not.toBeVisible();
75+
await expect(timelineLoadingIndicator).not.toBeVisible();
76+
await expect.poll(timelineCanvasContent, { timeout: 10000 }).toBe(true);
77+
78+
await setup.plan.reRunSimulation();
79+
await expect(timelineErrorIndicator).not.toBeVisible();
80+
await expect(timelineLoadingIndicator).not.toBeVisible();
81+
await expect.poll(timelineCanvasContent, { timeout: 10000 }).toBe(true);
82+
});
83+
6484
test(`Plans with an invalid activity should fail simulation`, async () => {
85+
const timelineLoadingIndicator = setup.plan.page.getByRole('status', { name: 'Timeline loading' });
6586
await setup.plan.addActivityByDragAndDrop('BakeBananaBread');
6687
await setup.plan.runSimulation(Status.Failed);
88+
// Regression: indicator must settle for terminal-null sims too.
89+
await expect(timelineLoadingIndicator).not.toBeVisible();
6790
});
6891

6992
test(`Modified plans should indicate that simulation is out of date`, async () => {

e2e-tests/utilities/canvas.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import type { Page } from '@playwright/test';
2+
3+
/**
4+
* True if any canvas matched by `selector` has a pixel with non-zero alpha.
5+
* Stronger than checking the canvas exists — a transparent canvas would
6+
* pass that. Pair with `expect.poll` for post-async-update checks.
7+
*/
8+
export function anyCanvasHasContent(page: Page, selector: string = 'canvas'): Promise<boolean> {
9+
return page.evaluate(sel => {
10+
const canvases = document.querySelectorAll<HTMLCanvasElement>(sel);
11+
for (const canvas of Array.from(canvases)) {
12+
const ctx = canvas.getContext('2d');
13+
if (!ctx) {
14+
continue;
15+
}
16+
const data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
17+
for (let i = 3; i < data.length; i += 4) {
18+
if (data[i] > 0) {
19+
return true;
20+
}
21+
}
22+
}
23+
return false;
24+
}, selector);
25+
}

src/components/ResourceList.svelte

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,7 @@
44
import CloseIcon from '@nasa-jpl/stellar/icons/close.svg?component';
55
import UploadIcon from '@nasa-jpl/stellar/icons/upload.svg?component';
66
import { plan } from '../stores/plan';
7-
import {
8-
allResourceTypes,
9-
fetchingResourcesExternal,
10-
resourceTypesLoading,
11-
simulationDatasetId,
12-
} from '../stores/simulation';
7+
import { allResourceTypes, resourceTypesLoading, simulationDatasetId } from '../stores/simulation';
138
import type { User } from '../types/app';
149
import type { ResourceType } from '../types/simulation';
1510
import type { TimelineItemType } from '../types/timeline';
@@ -34,7 +29,7 @@
3429
let loading: boolean = false;
3530
3631
$: resourceDataTypes = [...new Set($allResourceTypes.map(t => t.schema.type))];
37-
$: loading = $fetchingResourcesExternal || $resourceTypesLoading;
32+
$: loading = $resourceTypesLoading;
3833
$: if (user !== null && $plan !== null) {
3934
hasUploadPermission = featurePermissions.externalResources.canCreate(user, $plan);
4035
}

0 commit comments

Comments
 (0)