| 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 | 217 | const identity = yield* DesktopAppIdentity.DesktopAppIdentity; |
| 218 | 218 | yield* identity.configure; |
| 219 | 219 | |
| 220 | assert.deepEqual(calls.setName, ["T3 Code (Alpha)"]); | |
| 220 | assert.deepEqual(calls.setName, ["T3 Code Alpha"]); | |
| 221 | 221 | assert.equal(calls.setAboutPanelOptions[0]?.applicationName, "T3 Code (Alpha)"); |
| 222 | 222 | assert.equal(calls.setAboutPanelOptions[0]?.applicationVersion, "1.2.3"); |
| 223 | 223 | assert.equal(calls.setAboutPanelOptions[0]?.version, "0123456789ab"); |
| ... | ... | @@ -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 | 273 | it.effect("sets the dock icon only when running unpackaged", () => { |
| 241 | 274 | const calls: ElectronAppCalls = { |
| 242 | 275 | setAboutPanelOptions: [], |
apps/desktop/src/app/DesktopAppIdentity.ts+6-1| ... | ... | @@ -91,7 +91,12 @@ export const make = Effect.gen(function* () { |
| 91 | 91 | |
| 92 | 92 | const configure = Effect.gen(function* () { |
| 93 | 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 | 100 | yield* electronApp.setAboutPanelOptions({ |
| 96 | 101 | applicationName: environment.displayName, |
| 97 | 102 | applicationVersion: environment.appVersion, |
apps/desktop/src/window/DesktopApplicationMenu.test.ts+35-1| ... | ... | @@ -103,6 +103,7 @@ const layerElectronMenu = ( |
| 103 | 103 | const configureMenu = ( |
| 104 | 104 | selectedAction: Deferred.Deferred<string>, |
| 105 | 105 | applicationMenuTemplate: Deferred.Deferred<readonly Electron.MenuItemConstructorOptions[]>, |
| 106 | environment: Partial<DesktopEnvironment.MakeDesktopEnvironmentInput> = {}, | |
| 106 | 107 | ) => |
| 107 | 108 | Effect.gen(function* () { |
| 108 | 109 | const menu = yield* DesktopApplicationMenu.DesktopApplicationMenu; |
| ... | ... | @@ -116,7 +117,7 @@ const configureMenu = ( |
| 116 | 117 | Layer.provideMerge(layerElectronDialog), |
| 117 | 118 | Layer.provideMerge(layerElectronApp), |
| 118 | 119 | Layer.provideMerge( |
| 119 | DesktopEnvironment.layer(environmentInput).pipe( | |
| 120 | DesktopEnvironment.layer({ ...environmentInput, ...environment }).pipe( | |
| 120 | 121 | Layer.provide(Layer.mergeAll(NodeServices.layer, DesktopConfig.layerTest({}))), |
| 121 | 122 | ), |
| 122 | 123 | ), |
| ... | ... | @@ -125,6 +126,39 @@ const configureMenu = ( |
| 125 | 126 | ); |
| 126 | 127 | |
| 127 | 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 | 162 | it.effect("installs the native menu and routes Settings through DesktopWindow", () => |
| 129 | 163 | Effect.gen(function* () { |
| 130 | 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 | import type * as Electron from "electron"; |
| 8 | 8 | |
| 9 | 9 | import { makeComponentLogger } from "../app/DesktopObservability.ts"; |
| 10 | import * as ElectronApp from "../electron/ElectronApp.ts"; | |
| 11 | 10 | import * as ElectronDialog from "../electron/ElectronDialog.ts"; |
| 12 | 11 | import * as ElectronMenu from "../electron/ElectronMenu.ts"; |
| 13 | 12 | import * as DesktopEnvironment from "../app/DesktopEnvironment.ts"; |
| ... | ... | @@ -107,10 +106,8 @@ const handleCheckForUpdatesMenuClick = Effect.gen(function* () { |
| 107 | 106 | |
| 108 | 107 | /** @public Service construction is part of the canonical Effect module API. */ |
| 109 | 108 | export const make = Effect.gen(function* () { |
| 110 | const electronApp = yield* ElectronApp.ElectronApp; | |
| 111 | 109 | const electronMenu = yield* ElectronMenu.ElectronMenu; |
| 112 | 110 | const environment = yield* DesktopEnvironment.DesktopEnvironment; |
| 113 | const appName = yield* electronApp.name; | |
| 114 | 111 | const context = yield* Effect.context<DesktopApplicationMenuRuntimeServices>(); |
| 115 | 112 | const runPromise = Effect.runPromiseWith(context); |
| 116 | 113 | |
| ... | ... | @@ -157,9 +154,9 @@ export const make = Effect.gen(function* () { |
| 157 | 154 | |
| 158 | 155 | if (environment.platform === "darwin") { |
| 159 | 156 | template.push({ |
| 160 | label: appName, | |
| 157 | label: environment.displayName, | |
| 161 | 158 | submenu: [ |
| 162 | { role: "about" }, | |
| 159 | { role: "about", label: `About ${environment.displayName}` }, | |
| 163 | 160 | { |
| 164 | 161 | label: "Check for Updates...", |
| 165 | 162 | click: checkForUpdatesClick, |
| ... | ... | @@ -173,11 +170,11 @@ export const make = Effect.gen(function* () { |
| 173 | 170 | { type: "separator" }, |
| 174 | 171 | { role: "services" }, |
| 175 | 172 | { type: "separator" }, |
| 176 | { role: "hide" }, | |
| 173 | { role: "hide", label: `Hide ${environment.displayName}` }, | |
| 177 | 174 | { role: "hideOthers" }, |
| 178 | 175 | { role: "unhide" }, |
| 179 | 176 | { type: "separator" }, |
| 180 | { role: "quit" }, | |
| 177 | { role: "quit", label: `Quit ${environment.displayName}` }, | |
| 181 | 178 | ], |
| 182 | 179 | }); |
| 183 | 180 | } |