diff --git a/.github/RELEASE_CHECKLIST.md b/.github/RELEASE_CHECKLIST.md index 191f5f792..116fefd94 100644 --- a/.github/RELEASE_CHECKLIST.md +++ b/.github/RELEASE_CHECKLIST.md @@ -214,6 +214,22 @@ try { } ``` +For Windows installer shortcut verification, record the minimum matrix: + +- English Windows fresh install, `Just me`, desktop shortcut checked: current user desktop shortcut exists and launches PawWork +- English Windows fresh install, `All users`, desktop shortcut checked: public desktop shortcut exists and launches PawWork +- Chinese Windows fresh install, `Just me`, desktop shortcut checked: current user desktop shortcut is `爪印.lnk` and launches PawWork +- Chinese Windows fresh install, `All users`, desktop shortcut checked: public desktop shortcut is `爪印.lnk` and launches PawWork +- unchecked install: no desktop shortcut is created and the Start Menu entry still launches PawWork +- reinstall with desktop shortcut checked: missing standard desktop shortcut is repaired +- reinstall with desktop shortcut unchecked: existing desktop shortcut state is left unchanged +- scope switch between `Just me` and `All users`: standard desktop shortcut only exists in the selected install scope +- Chinese reinstall over an older standard `PawWork.lnk`: standard desktop shortcut migrates to `爪印.lnk` +- app language change after install: desktop shortcut name is not changed +- auto-update from the previous affected version: existing desktop shortcut state is left unchanged, including the no-desktop-shortcut state + +Do not close the Windows desktop shortcut issue until this real Windows installer evidence is recorded. + Keep `.zip`, `.blockmap`, and `latest*.yml` assets unless updater requirements are proven safe without them. If verification fails, check the reported missing or malformed asset first, rerun only the affected build phase, and publish the release only after the verification helper passes. diff --git a/packages/desktop-electron/electron-builder-app-update.test.ts b/packages/desktop-electron/electron-builder-app-update.test.ts index 556bb0aa3..2d01796b0 100644 --- a/packages/desktop-electron/electron-builder-app-update.test.ts +++ b/packages/desktop-electron/electron-builder-app-update.test.ts @@ -61,6 +61,19 @@ describe("electron builder app-update config", () => { }) }) + test("windows nsis installer uses PawWork shortcut customizations", () => { + const config = createConfig("prod") + + expect(config.nsis).toMatchObject({ + oneClick: false, + allowToChangeInstallationDirectory: true, + createDesktopShortcut: false, + createStartMenuShortcut: true, + include: "resources/installer.nsh", + installerLanguages: ["en_US", "zh_CN"], + }) + }) + test("all channels share the versioned artifact name", () => { expect(createConfig("dev").artifactName).toBe("pawwork-${os}-${arch}-${version}.${ext}") expect(createConfig("beta").artifactName).toBe("pawwork-${os}-${arch}-${version}.${ext}") diff --git a/packages/desktop-electron/electron-builder-nsis-shortcut.test.ts b/packages/desktop-electron/electron-builder-nsis-shortcut.test.ts new file mode 100644 index 000000000..6abf6d45b --- /dev/null +++ b/packages/desktop-electron/electron-builder-nsis-shortcut.test.ts @@ -0,0 +1,89 @@ +import { describe, expect, test } from "bun:test" +import { readFileSync } from "node:fs" +import { join } from "node:path" + +const script = readFileSync(join(import.meta.dir, "resources", "installer.nsh"), "utf8") + +describe("windows nsis desktop shortcut customization", () => { + test("adds an assisted installer checkbox with English and Chinese labels", () => { + expect(script).toContain("AddDesktopShortcut") + expect(script).toContain("添加桌面快捷方式") + expect(script).toContain("Add desktop shortcut") + expect(script).toContain("Shortcut Options") + expect(script).toContain("快捷方式选项") + expect(script).toContain('LangString PawWorkAddDesktopShortcut 1033 "Add desktop shortcut"') + expect(script).toContain('LangString PawWorkAddDesktopShortcut 2052 "添加桌面快捷方式"') + expect(script).toContain('LangString PawWorkShortcutOptions 1033 "Shortcut Options"') + expect(script).toContain('LangString PawWorkShortcutOptions 2052 "快捷方式选项"') + expect(script).not.toContain("LANG_ENGLISH") + expect(script).not.toContain("LANG_SIMPCHINESE") + expect(script).toContain("BST_CHECKED") + }) + + test("uses language-aware standard shortcut names", () => { + expect(script).toContain("PawWork") + expect(script).toContain("爪印") + expect(script).toContain("爪印 Beta") + expect(script).toContain("爪印 Dev") + expect(script).toContain("$LANGUAGE") + }) + + test("does not mutate desktop shortcuts during auto-update", () => { + expect(script).toContain("!include FileFunc.nsh") + expect(script).toContain('"--updated"') + expect(script).not.toContain("${isUpdated}") + expect(script).not.toContain("!insertmacro skipPageIfUpdated") + expect(script).toContain("PAWWORK_SKIP_DESKTOP_SHORTCUT") + }) + + test("keeps custom renamed shortcuts out of scope", () => { + expect(script).toContain("PAWWORK_STANDARD_SHORTCUT") + expect(script).not.toContain("我的 AI 工具") + }) + + test("declares a real custom page instead of running page commands inline", () => { + expect(script).toContain("!ifndef BUILD_UNINSTALLER") + expect(script).toContain("!ifndef BUILD_UNINSTALLER\n Var AddDesktopShortcutCheckbox") + expect(script).toContain("PageEx custom") + expect(script).toContain('Caption "$(PawWorkShortcutOptions)"') + expect(script).toContain("PageCallbacks PawWorkDesktopShortcutPageCreate PawWorkDesktopShortcutPageLeave") + expect(script).toContain('Function "PawWorkDesktopShortcutPageCreate"') + expect(script).toContain('Function "PawWorkDesktopShortcutPageLeave"') + }) + + test("uses channel-specific shortcut names instead of hard-coded prod names", () => { + expect(script).toContain("${SHORTCUT_NAME}") + expect(script).toContain('${AndIf} "${SHORTCUT_NAME}" == "PawWork"') + expect(script).toContain('${AndIf} "${SHORTCUT_NAME}" == "PawWork Beta"') + expect(script).toContain('${AndIf} "${SHORTCUT_NAME}" == "PawWork Dev"') + expect(script).toContain('Delete "$DESKTOP\\${SHORTCUT_NAME}.lnk"') + expect(script).toContain('Delete "$DESKTOP\\爪印.lnk"') + expect(script).toContain('Delete "$DESKTOP\\爪印 Beta.lnk"') + expect(script).toContain('Delete "$DESKTOP\\爪印 Dev.lnk"') + }) + + test("cleans standard shortcuts across scopes only for checked installs", () => { + expect(script).toContain("PAWWORK_REMOVE_STANDARD_SHORTCUTS_IN_ALL_INSTALL_SCOPES") + expect(script).toMatch( + /\$AddDesktopShortcut == \$\{BST_CHECKED\}[\s\S]*PAWWORK_REMOVE_STANDARD_SHORTCUTS_IN_ALL_INSTALL_SCOPES[\s\S]*PAWWORK_RESTORE_INSTALL_SCOPE[\s\S]*CreateShortCut/, + ) + }) + + test("uses an elevated public desktop cleanup when switching from all-users to just-me", () => { + expect(script).toContain("PAWWORK_REMOVE_PUBLIC_STANDARD_SHORTCUTS_ELEVATED") + expect(script).toContain('${IfNot} ${UAC_IsAdmin}') + expect(script).toContain('$installMode != "all"') + expect(script).toContain('$hasPerMachineInstallation == "1"') + expect(script).toContain('${StdUtils.ExecShellWaitEx} $0 $1 "$SYSDIR\\cmd.exe" "runas"') + }) + + test("owns uninstall cleanup for standard shortcuts in the selected install scope", () => { + expect(script).toContain("customUnInstall") + expect(script).toContain("PAWWORK_REMOVE_STANDARD_SHORTCUTS") + expect(script).not.toContain("PAWWORK_REMOVE_STANDARD_SHORTCUTS_IN_BOTH_SCOPES") + expect(script).toContain("SetShellVarContext current") + expect(script).toContain("SetShellVarContext all") + expect(script).toContain("PAWWORK_RESTORE_INSTALL_SCOPE") + expect(script).toMatch(/PAWWORK_RESTORE_INSTALL_SCOPE\s+!insertmacro PAWWORK_REMOVE_STANDARD_SHORTCUTS/) + }) +}) diff --git a/packages/desktop-electron/electron-builder.config.ts b/packages/desktop-electron/electron-builder.config.ts index 40502a201..6e2821b32 100644 --- a/packages/desktop-electron/electron-builder.config.ts +++ b/packages/desktop-electron/electron-builder.config.ts @@ -160,6 +160,10 @@ const getBase = (): Configuration => ({ nsis: { oneClick: false, allowToChangeInstallationDirectory: true, + createDesktopShortcut: false, + createStartMenuShortcut: true, + include: "resources/installer.nsh", + installerLanguages: ["en_US", "zh_CN"], installerIcon: `resources/icons/icon.ico`, installerHeaderIcon: `resources/icons/icon.ico`, }, diff --git a/packages/desktop-electron/resources/installer.nsh b/packages/desktop-electron/resources/installer.nsh new file mode 100644 index 000000000..8583c07a9 --- /dev/null +++ b/packages/desktop-electron/resources/installer.nsh @@ -0,0 +1,141 @@ +!include nsDialogs.nsh +!include LogicLib.nsh +!include FileFunc.nsh +!include StdUtils.nsh +!include UAC.nsh + +Var PawWorkStandardShortcutName + +LangString PawWorkAddDesktopShortcut 1033 "Add desktop shortcut" +LangString PawWorkAddDesktopShortcut 2052 "添加桌面快捷方式" +LangString PawWorkShortcutOptions 1033 "Shortcut Options" +LangString PawWorkShortcutOptions 2052 "快捷方式选项" + +!macro PAWWORK_STANDARD_SHORTCUT + StrCpy $PawWorkStandardShortcutName "${SHORTCUT_NAME}" + ${If} $LANGUAGE == 2052 + ${AndIf} "${SHORTCUT_NAME}" == "PawWork" + StrCpy $PawWorkStandardShortcutName "爪印" + ${ElseIf} $LANGUAGE == 2052 + ${AndIf} "${SHORTCUT_NAME}" == "PawWork Beta" + StrCpy $PawWorkStandardShortcutName "爪印 Beta" + ${ElseIf} $LANGUAGE == 2052 + ${AndIf} "${SHORTCUT_NAME}" == "PawWork Dev" + StrCpy $PawWorkStandardShortcutName "爪印 Dev" + ${EndIf} +!macroend + +!macro PAWWORK_REMOVE_STANDARD_SHORTCUTS + !insertmacro PAWWORK_STANDARD_SHORTCUT + Delete "$DESKTOP\$PawWorkStandardShortcutName.lnk" + Delete "$DESKTOP\${SHORTCUT_NAME}.lnk" + ${If} "${SHORTCUT_NAME}" == "PawWork" + Delete "$DESKTOP\PawWork.lnk" + Delete "$DESKTOP\爪印.lnk" + ${ElseIf} "${SHORTCUT_NAME}" == "PawWork Beta" + Delete "$DESKTOP\PawWork Beta.lnk" + Delete "$DESKTOP\爪印 Beta.lnk" + ${ElseIf} "${SHORTCUT_NAME}" == "PawWork Dev" + Delete "$DESKTOP\PawWork Dev.lnk" + Delete "$DESKTOP\爪印 Dev.lnk" + ${EndIf} +!macroend + +!macro PAWWORK_REMOVE_PUBLIC_STANDARD_SHORTCUTS_ELEVATED + # A per-user reinstall cannot remove Public Desktop shortcuts without elevation. + ${IfNot} ${UAC_IsAdmin} + ${AndIf} $installMode != "all" + ${AndIf} $hasPerMachineInstallation == "1" + SetShellVarContext all + ${If} "${SHORTCUT_NAME}" == "PawWork" + ${StdUtils.ExecShellWaitEx} $0 $1 "$SYSDIR\cmd.exe" "runas" '/C del /F /Q "$DESKTOP\PawWork.lnk" "$DESKTOP\爪印.lnk"' + ${ElseIf} "${SHORTCUT_NAME}" == "PawWork Beta" + ${StdUtils.ExecShellWaitEx} $0 $1 "$SYSDIR\cmd.exe" "runas" '/C del /F /Q "$DESKTOP\PawWork Beta.lnk" "$DESKTOP\爪印 Beta.lnk"' + ${ElseIf} "${SHORTCUT_NAME}" == "PawWork Dev" + ${StdUtils.ExecShellWaitEx} $0 $1 "$SYSDIR\cmd.exe" "runas" '/C del /F /Q "$DESKTOP\PawWork Dev.lnk" "$DESKTOP\爪印 Dev.lnk"' + ${EndIf} + ${EndIf} +!macroend + +!macro PAWWORK_REMOVE_STANDARD_SHORTCUTS_IN_ALL_INSTALL_SCOPES + SetShellVarContext current + !insertmacro PAWWORK_REMOVE_STANDARD_SHORTCUTS + SetShellVarContext all + !insertmacro PAWWORK_REMOVE_STANDARD_SHORTCUTS + !insertmacro PAWWORK_REMOVE_PUBLIC_STANDARD_SHORTCUTS_ELEVATED +!macroend + +!macro PAWWORK_RESTORE_INSTALL_SCOPE + ${If} $installMode == "all" + SetShellVarContext all + ${Else} + SetShellVarContext current + ${EndIf} +!macroend + +!ifndef BUILD_UNINSTALLER + Var AddDesktopShortcutCheckbox + Var AddDesktopShortcut + + !macro customPageAfterChangeDir + PageEx custom + PageCallbacks PawWorkDesktopShortcutPageCreate PawWorkDesktopShortcutPageLeave + Caption "$(PawWorkShortcutOptions)" + PageExEnd + !macroend + + Function "PawWorkDesktopShortcutPageCreate" + ClearErrors + ${GetParameters} $0 + ${GetOptions} $0 "--updated" $1 + ${IfNot} ${Errors} + Abort + ${EndIf} + + nsDialogs::Create 1018 + Pop $0 + ${If} $0 == error + Abort + ${EndIf} + + ${NSD_CreateCheckbox} 0 0 100% 12u "$(PawWorkAddDesktopShortcut)" + Pop $AddDesktopShortcutCheckbox + ${NSD_Check} $AddDesktopShortcutCheckbox + nsDialogs::Show + FunctionEnd + + Function "PawWorkDesktopShortcutPageLeave" + StrCpy $AddDesktopShortcut "0" + ${NSD_GetState} $AddDesktopShortcutCheckbox $AddDesktopShortcut + FunctionEnd + + !macro customInstall + ClearErrors + ${GetParameters} $0 + ${GetOptions} $0 "--updated" $1 + ${IfNot} ${Errors} + StrCpy $AddDesktopShortcut "PAWWORK_SKIP_DESKTOP_SHORTCUT" + ${EndIf} + + ${If} $AddDesktopShortcut == ${BST_CHECKED} + !insertmacro PAWWORK_STANDARD_SHORTCUT + !insertmacro PAWWORK_REMOVE_STANDARD_SHORTCUTS_IN_ALL_INSTALL_SCOPES + !insertmacro PAWWORK_RESTORE_INSTALL_SCOPE + CreateShortCut "$DESKTOP\$PawWorkStandardShortcutName.lnk" "$appExe" "" "$appExe" 0 "" "" "${APP_DESCRIPTION}" + ClearErrors + WinShell::SetLnkAUMI "$DESKTOP\$PawWorkStandardShortcutName.lnk" "${APP_ID}" + ${EndIf} + !macroend +!endif + +!ifdef BUILD_UNINSTALLER + !macro customUnInstall + ClearErrors + ${GetParameters} $0 + ${GetOptions} $0 "--updated" $1 + ${If} ${Errors} + !insertmacro PAWWORK_RESTORE_INSTALL_SCOPE + !insertmacro PAWWORK_REMOVE_STANDARD_SHORTCUTS + ${EndIf} + !macroend +!endif diff --git a/packages/desktop-electron/scripts/release-workflow-contract.test.ts b/packages/desktop-electron/scripts/release-workflow-contract.test.ts index e7703206f..f9d8ec63b 100644 --- a/packages/desktop-electron/scripts/release-workflow-contract.test.ts +++ b/packages/desktop-electron/scripts/release-workflow-contract.test.ts @@ -52,3 +52,21 @@ describe("release workflow app-update verification", () => { expect(workflow).toContain('officecli_platform="win32"') }) }) + +const checklist = readFileSync(join(import.meta.dir, "..", "..", "..", ".github", "RELEASE_CHECKLIST.md"), "utf8") + +describe("release checklist Windows installer verification", () => { + test("records the Windows desktop shortcut verification matrix", () => { + expect(checklist).toContain("English Windows fresh install") + expect(checklist).toContain("Chinese Windows fresh install") + expect(checklist).toContain("unchecked install") + expect(checklist).toContain("reinstall with desktop shortcut checked") + expect(checklist).toContain("reinstall with desktop shortcut unchecked") + expect(checklist).toContain("scope switch between `Just me` and `All users`") + expect(checklist).toContain("older standard `PawWork.lnk`") + expect(checklist).toContain("app language change after install") + expect(checklist).toContain("previous affected version") + expect(checklist).toContain("no-desktop-shortcut state") + expect(checklist).toContain("Do not close the Windows desktop shortcut issue") + }) +})