| author | |
| committer | |
| log | a0066d5cb7d18abd786c1c23bf835f898490fd37 |
| tree | 9bd88c369e1cda8597af0b5f380c7f6c64843c2d |
| parent | e8f5850b824572bab76bf810696f875eb648eff9 |
| signature | Signed by PGP key B5690EEEBB952194 |
4 files changed, 79 insertions(+), 10 deletions(-)
apps/desktop/src/app/DesktopAppIdentity.test.ts+34-1| ... | @@ -217,7 +217,7 @@ describe("DesktopAppIdentity", () => { | ... | @@ -217,7 +217,7 @@ describe("DesktopAppIdentity", () => { |
| 217 | const identity = yield* DesktopAppIdentity.DesktopAppIdentity; | 217 | const identity = yield* DesktopAppIdentity.DesktopAppIdentity; |
| 218 | yield* identity.configure; | 218 | yield* identity.configure; |
| 219 | 219 | ||
| 220 | assert.deepEqual(calls.setName, ["T3 Code (Alpha)"]); | 220 | assert.deepEqual(calls.setName, ["T3 Code Alpha"]); |
| 221 | assert.equal(calls.setAboutPanelOptions[0]?.applicationName, "T3 Code (Alpha)"); | 221 | assert.equal(calls.setAboutPanelOptions[0]?.applicationName, "T3 Code (Alpha)"); |
| 222 | assert.equal(calls.setAboutPanelOptions[0]?.applicationVersion, "1.2.3"); | 222 | assert.equal(calls.setAboutPanelOptions[0]?.applicationVersion, "1.2.3"); |
| 223 | assert.equal(calls.setAboutPanelOptions[0]?.version, "0123456789ab"); | 223 | assert.equal(calls.setAboutPanelOptions[0]?.version, "0123456789ab"); |
| ... | @@ -237,6 +237,39 @@ describe("DesktopAppIdentity", () => { | ... | @@ -237,6 +237,39 @@ describe("DesktopAppIdentity", () => { |
| 237 | ); | 237 | ); |
| 238 | }); | 238 | }); |
| 239 | 239 | ||
| 240 | it.effect.each([ | ||
| 241 | { stage: "Alpha", environment: {} }, | ||
| 242 | { | ||
| 243 | stage: "Nightly", | ||
| 244 | environment: { appVersion: "0.0.43-nightly.20260929.2428" }, | ||
| 245 | }, | ||
| 246 | { | ||
| 247 | stage: "Dev", | ||
| 248 | environment: { env: { VITE_DEV_SERVER_URL: "http://localhost:5173" } }, | ||
| 249 | }, | ||
| 250 | ])("uses a valid native User-Agent product name for $stage", ({ stage, environment }) => { | ||
| 251 | const calls: ElectronAppCalls = { | ||
| 252 | setAboutPanelOptions: [], | ||
| 253 | setDockIcon: [], | ||
| 254 | setName: [], | ||
| 255 | }; | ||
| 256 | |||
| 257 | return withIdentity( | ||
| 258 | Effect.gen(function* () { | ||
| 259 | const identity = yield* DesktopAppIdentity.DesktopAppIdentity; | ||
| 260 | yield* identity.configure; | ||
| 261 | |||
| 262 | const runtimeName = calls.setName[0]; | ||
| 263 | assert.isDefined(runtimeName); | ||
| 264 | assert.equal(runtimeName, `T3 Code ${stage}`); | ||
| 265 | // RFC 9110's token grammar, after Electron removes ASCII spaces. | ||
| 266 | assert.match(runtimeName.replaceAll(" ", ""), /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/); | ||
| 267 | assert.equal(calls.setAboutPanelOptions[0]?.applicationName, `T3 Code (${stage})`); | ||
| 268 | }), | ||
| 269 | { calls, environment }, | ||
| 270 | ); | ||
| 271 | }); | ||
| 272 | |||
| 240 | it.effect("sets the dock icon only when running unpackaged", () => { | 273 | it.effect("sets the dock icon only when running unpackaged", () => { |
| 241 | const calls: ElectronAppCalls = { | 274 | const calls: ElectronAppCalls = { |
| 242 | setAboutPanelOptions: [], | 275 | setAboutPanelOptions: [], |
apps/desktop/src/app/DesktopAppIdentity.ts+6-1| ... | @@ -91,7 +91,12 @@ export const make = Effect.gen(function* () { | ... | @@ -91,7 +91,12 @@ export const make = Effect.gen(function* () { |
| 91 | 91 | ||
| 92 | const configure = Effect.gen(function* () { | 92 | const configure = Effect.gen(function* () { |
| 93 | const commitHash = yield* resolveAboutCommitHash; | 93 | const commitHash = yield* resolveAboutCommitHash; |
| 94 | yield* electronApp.setName(environment.displayName); | 94 | // Electron removes spaces from this name to build the native User-Agent |
| 95 | // product token, but leaves parentheses intact. Keep the runtime name valid | ||
| 96 | // without rewriting preview sessions (which breaks Turnstile, #7110). | ||
| 97 | yield* electronApp.setName( | ||
| 98 | `${environment.branding.baseName} ${environment.branding.stageLabel}`, | ||
| 99 | ); | ||
| 95 | yield* electronApp.setAboutPanelOptions({ | 100 | yield* electronApp.setAboutPanelOptions({ |
| 96 | applicationName: environment.displayName, | 101 | applicationName: environment.displayName, |
| 97 | applicationVersion: environment.appVersion, | 102 | applicationVersion: environment.appVersion, |
apps/desktop/src/window/DesktopApplicationMenu.test.ts+35-1| ... | @@ -103,6 +103,7 @@ const layerElectronMenu = ( | ... | @@ -103,6 +103,7 @@ const layerElectronMenu = ( |
| 103 | const configureMenu = ( | 103 | const configureMenu = ( |
| 104 | selectedAction: Deferred.Deferred<string>, | 104 | selectedAction: Deferred.Deferred<string>, |
| 105 | applicationMenuTemplate: Deferred.Deferred<readonly Electron.MenuItemConstructorOptions[]>, | 105 | applicationMenuTemplate: Deferred.Deferred<readonly Electron.MenuItemConstructorOptions[]>, |
| 106 | environment: Partial<DesktopEnvironment.MakeDesktopEnvironmentInput> = {}, | ||
| 106 | ) => | 107 | ) => |
| 107 | Effect.gen(function* () { | 108 | Effect.gen(function* () { |
| 108 | const menu = yield* DesktopApplicationMenu.DesktopApplicationMenu; | 109 | const menu = yield* DesktopApplicationMenu.DesktopApplicationMenu; |
| ... | @@ -116,7 +117,7 @@ const configureMenu = ( | ... | @@ -116,7 +117,7 @@ const configureMenu = ( |
| 116 | Layer.provideMerge(layerElectronDialog), | 117 | Layer.provideMerge(layerElectronDialog), |
| 117 | Layer.provideMerge(layerElectronApp), | 118 | Layer.provideMerge(layerElectronApp), |
| 118 | Layer.provideMerge( | 119 | Layer.provideMerge( |
| 119 | DesktopEnvironment.layer(environmentInput).pipe( | 120 | DesktopEnvironment.layer({ ...environmentInput, ...environment }).pipe( |
| 120 | Layer.provide(Layer.mergeAll(NodeServices.layer, DesktopConfig.layerTest({}))), | 121 | Layer.provide(Layer.mergeAll(NodeServices.layer, DesktopConfig.layerTest({}))), |
| 121 | ), | 122 | ), |
| 122 | ), | 123 | ), |
| ... | @@ -125,6 +126,39 @@ const configureMenu = ( | ... | @@ -125,6 +126,39 @@ const configureMenu = ( |
| 125 | ); | 126 | ); |
| 126 | 127 | ||
| 127 | describe("DesktopApplicationMenu", () => { | 128 | describe("DesktopApplicationMenu", () => { |
| 129 | it.effect("keeps display branding in the macOS application menu", () => | ||
| 130 | Effect.gen(function* () { | ||
| 131 | const selectedAction = yield* Deferred.make<string>(); | ||
| 132 | const applicationMenuTemplate = | ||
| 133 | yield* Deferred.make<readonly Electron.MenuItemConstructorOptions[]>(); | ||
| 134 | |||
| 135 | yield* configureMenu(selectedAction, applicationMenuTemplate, { | ||
| 136 | platform: "darwin", | ||
| 137 | appVersion: "0.0.43-nightly.20260929.2428", | ||
| 138 | }); | ||
| 139 | |||
| 140 | const template = yield* Deferred.await(applicationMenuTemplate); | ||
| 141 | const applicationMenu = template[0]; | ||
| 142 | assert.isDefined(applicationMenu); | ||
| 143 | assert.equal(applicationMenu.label, "T3 Code (Nightly)"); | ||
| 144 | if (!Array.isArray(applicationMenu.submenu)) { | ||
| 145 | throw new Error("Expected application menu submenu to be an array."); | ||
| 146 | } | ||
| 147 | assert.equal( | ||
| 148 | applicationMenu.submenu.find((item) => item.role === "about")?.label, | ||
| 149 | "About T3 Code (Nightly)", | ||
| 150 | ); | ||
| 151 | assert.equal( | ||
| 152 | applicationMenu.submenu.find((item) => item.role === "hide")?.label, | ||
| 153 | "Hide T3 Code (Nightly)", | ||
| 154 | ); | ||
| 155 | assert.equal( | ||
| 156 | applicationMenu.submenu.find((item) => item.role === "quit")?.label, | ||
| 157 | "Quit T3 Code (Nightly)", | ||
| 158 | ); | ||
| 159 | }), | ||
| 160 | ); | ||
| 161 | |||
| 128 | it.effect("installs the native menu and routes Settings through DesktopWindow", () => | 162 | it.effect("installs the native menu and routes Settings through DesktopWindow", () => |
| 129 | Effect.gen(function* () { | 163 | Effect.gen(function* () { |
| 130 | const selectedAction = yield* Deferred.make<string>(); | 164 | const selectedAction = yield* Deferred.make<string>(); |
apps/desktop/src/window/DesktopApplicationMenu.ts+4-7| ... | @@ -7,7 +7,6 @@ import * as Schema from "effect/Schema"; | ... | @@ -7,7 +7,6 @@ import * as Schema from "effect/Schema"; |
| 7 | import type * as Electron from "electron"; | 7 | import type * as Electron from "electron"; |
| 8 | 8 | ||
| 9 | import { makeComponentLogger } from "../app/DesktopObservability.ts"; | 9 | import { makeComponentLogger } from "../app/DesktopObservability.ts"; |
| 10 | import * as ElectronApp from "../electron/ElectronApp.ts"; | ||
| 11 | import * as ElectronDialog from "../electron/ElectronDialog.ts"; | 10 | import * as ElectronDialog from "../electron/ElectronDialog.ts"; |
| 12 | import * as ElectronMenu from "../electron/ElectronMenu.ts"; | 11 | import * as ElectronMenu from "../electron/ElectronMenu.ts"; |
| 13 | import * as DesktopEnvironment from "../app/DesktopEnvironment.ts"; | 12 | import * as DesktopEnvironment from "../app/DesktopEnvironment.ts"; |
| ... | @@ -107,10 +106,8 @@ const handleCheckForUpdatesMenuClick = Effect.gen(function* () { | ... | @@ -107,10 +106,8 @@ const handleCheckForUpdatesMenuClick = Effect.gen(function* () { |
| 107 | 106 | ||
| 108 | /** @public Service construction is part of the canonical Effect module API. */ | 107 | /** @public Service construction is part of the canonical Effect module API. */ |
| 109 | export const make = Effect.gen(function* () { | 108 | export const make = Effect.gen(function* () { |
| 110 | const electronApp = yield* ElectronApp.ElectronApp; | ||
| 111 | const electronMenu = yield* ElectronMenu.ElectronMenu; | 109 | const electronMenu = yield* ElectronMenu.ElectronMenu; |
| 112 | const environment = yield* DesktopEnvironment.DesktopEnvironment; | 110 | const environment = yield* DesktopEnvironment.DesktopEnvironment; |
| 113 | const appName = yield* electronApp.name; | ||
| 114 | const context = yield* Effect.context<DesktopApplicationMenuRuntimeServices>(); | 111 | const context = yield* Effect.context<DesktopApplicationMenuRuntimeServices>(); |
| 115 | const runPromise = Effect.runPromiseWith(context); | 112 | const runPromise = Effect.runPromiseWith(context); |
| 116 | 113 | ||
| ... | @@ -157,9 +154,9 @@ export const make = Effect.gen(function* () { | ... | @@ -157,9 +154,9 @@ export const make = Effect.gen(function* () { |
| 157 | 154 | ||
| 158 | if (environment.platform === "darwin") { | 155 | if (environment.platform === "darwin") { |
| 159 | template.push({ | 156 | template.push({ |
| 160 | label: appName, | 157 | label: environment.displayName, |
| 161 | submenu: [ | 158 | submenu: [ |
| 162 | { role: "about" }, | 159 | { role: "about", label: `About ${environment.displayName}` }, |
| 163 | { | 160 | { |
| 164 | label: "Check for Updates...", | 161 | label: "Check for Updates...", |
| 165 | click: checkForUpdatesClick, | 162 | click: checkForUpdatesClick, |
| ... | @@ -173,11 +170,11 @@ export const make = Effect.gen(function* () { | ... | @@ -173,11 +170,11 @@ export const make = Effect.gen(function* () { |
| 173 | { type: "separator" }, | 170 | { type: "separator" }, |
| 174 | { role: "services" }, | 171 | { role: "services" }, |
| 175 | { type: "separator" }, | 172 | { type: "separator" }, |
| 176 | { role: "hide" }, | 173 | { role: "hide", label: `Hide ${environment.displayName}` }, |
| 177 | { role: "hideOthers" }, | 174 | { role: "hideOthers" }, |
| 178 | { role: "unhide" }, | 175 | { role: "unhide" }, |
| 179 | { type: "separator" }, | 176 | { type: "separator" }, |
| 180 | { role: "quit" }, | 177 | { role: "quit", label: `Quit ${environment.displayName}` }, |
| 181 | ], | 178 | ], |
| 182 | }); | 179 | }); |
| 183 | } | 180 | } |