diff --git a/ui/frontend/tests/e2e/fixtures/order-fbs.ts b/ui/frontend/tests/e2e/fixtures/order-fbs.ts index ac9fe20..b5e8c57 100644 --- a/ui/frontend/tests/e2e/fixtures/order-fbs.ts +++ b/ui/frontend/tests/e2e/fixtures/order-fbs.ts @@ -34,6 +34,10 @@ interface CommandResultFixtureBase { cmdId: string; applied: boolean | null; errorCode: number | null; + // Optional engine-formatted rejection reason; mirrors the + // `cmd_error_message` field added in the same patch as + // `cmdErrorCode`'s shelf renumber. Omit on applied commands. + errorMessage?: string | null; } export interface PlanetRenameResultFixture extends CommandResultFixtureBase { @@ -132,6 +136,14 @@ export function buildOrderResponsePayload( updatedAt: number, ): Uint8Array { const builder = new Builder(256); + // flatbuffers@25 elides any field equal to its generated default; + // for `cmd_applied: bool = null` the default is int8 0, so the + // false case (per-command rejection) would silently disappear from + // the encoded payload. The production Go transcoder uses explicit + // `Slot()` calls that force the write, and the unit-test helpers + // in `tests/helpers/fake-order-client.ts` flip `forceDefaults`; + // these e2e fixtures need the same toggle. + builder.forceDefaults(true); const itemOffsets = commands.map((c) => encodeItem(builder, c)); const commandsVec = UserGamesOrderResponse.createCommandsVector( builder, @@ -155,6 +167,11 @@ export function buildOrderGetResponsePayload( found = true, ): Uint8Array { const builder = new Builder(256); + // See `buildOrderResponsePayload` — the GET response path needs + // the same `forceDefaults` toggle so a stored per-command rejection + // (cmd_applied=false / cmd_error_code=0) survives the round trip + // to the UI's `hydrateFromServer`. + builder.forceDefaults(true); let orderOffset = 0; if (found) { @@ -290,6 +307,10 @@ function encodeItem(builder: Builder, c: CommandResultFixture): number { break; } } + const errMsgOffset = + c.errorMessage !== undefined && c.errorMessage !== null + ? builder.createString(c.errorMessage) + : 0; CommandItem.startCommandItem(builder); CommandItem.addCmdId(builder, cmdIdOffset); if (c.applied !== null) CommandItem.addCmdApplied(builder, c.applied); @@ -298,6 +319,9 @@ function encodeItem(builder: Builder, c: CommandResultFixture): number { } CommandItem.addPayloadType(builder, payloadType); CommandItem.addPayload(builder, inner); + if (errMsgOffset !== 0) { + CommandItem.addCmdErrorMessage(builder, errMsgOffset); + } return CommandItem.endCommandItem(builder); } diff --git a/ui/frontend/tests/e2e/rename-planet.spec.ts b/ui/frontend/tests/e2e/rename-planet.spec.ts index 25c7170..558a551 100644 --- a/ui/frontend/tests/e2e/rename-planet.spec.ts +++ b/ui/frontend/tests/e2e/rename-planet.spec.ts @@ -137,6 +137,9 @@ async function mockGateway(page: Page, opts: MockOpts): Promise { name: submittedName, applied, errorCode: applied ? null : 1, + errorMessage: applied + ? null + : "Entity does not exists: planet #99", }); } if (opts.submitOutcome === "applied") { @@ -320,14 +323,19 @@ test("rejected submit keeps the old name and surfaces the failure", async ({ const orderTool = page.getByTestId("sidebar-tool-order"); // The auto-sync pipeline reaches the server immediately after - // the inline confirm; the rejected verdict surfaces through the - // per-row status badge and the sync bar. + // the inline confirm. Per-command rejection is a player-correctable + // state: the round trip succeeded, the engine just refused this + // command, so the sync bar stays `synced` while the rejected row + // surfaces the engine-formatted reason for the player. await expect(orderTool.getByTestId("order-command-status-0")).toHaveText( "rejected", ); + await expect(orderTool.getByTestId("order-command-error-0")).toHaveText( + "Entity does not exists: planet #99", + ); await expect(orderTool.getByTestId("order-sync")).toHaveAttribute( "data-sync-status", - "error", + "synced", ); await page.getByTestId("sidebar-tab-inspector").click(); diff --git a/ui/frontend/tests/e2e/ship-classes.spec.ts b/ui/frontend/tests/e2e/ship-classes.spec.ts index 5742f65..d40afc3 100644 --- a/ui/frontend/tests/e2e/ship-classes.spec.ts +++ b/ui/frontend/tests/e2e/ship-classes.spec.ts @@ -167,6 +167,9 @@ async function mockGateway(page: Page, opts: MockOpts): Promise { cargo: lastCreate.cargo, applied, errorCode: applied ? null : 1, + errorMessage: applied + ? null + : "Entity already exists: ship type \"Drone\"", }); continue; } @@ -392,9 +395,10 @@ test("rejected createShipClass keeps the table empty and surfaces the failure", await expect(orderTool.getByTestId("order-command-status-0")).toHaveText( "rejected", ); + await expect(orderTool.getByTestId("order-command-error-0")).toBeVisible(); await expect(orderTool.getByTestId("order-sync")).toHaveAttribute( "data-sync-status", - "error", + "synced", ); // Switch sidebar back to inspector so the active-view (table)