513 lines
16 KiB
JavaScript
513 lines
16 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { readFileSync } from 'node:fs';
|
|
import { dirname, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const scriptsDir = dirname(fileURLToPath(import.meta.url));
|
|
const appRoot = join(scriptsDir, '..');
|
|
const repoRoot = join(appRoot, '..', '..');
|
|
const scriptPath = join(scriptsDir, 'windows-package-msvc.ps1');
|
|
const launcherPath = join(scriptsDir, 'windows-package-msvc.cmd');
|
|
const ciWorkflowPath = join(repoRoot, '.github', 'workflows', 'release-tauri.yml');
|
|
const imeBuildPath = join(scriptsDir, 'windows-ime-build.ps1');
|
|
const imeInstallSmokePath = join(scriptsDir, 'windows-ime-install-smoke.ps1');
|
|
const realAsrInsertionSmokePath = join(scriptsDir, 'windows-real-asr-insertion-smoke.ps1');
|
|
const imeRegisterPath = join(scriptsDir, 'windows-ime-register.ps1');
|
|
const imeUnregisterPath = join(scriptsDir, 'windows-ime-unregister.ps1');
|
|
const imeSolutionPath = join(appRoot, 'windows-ime', 'OpenLessIme.sln');
|
|
const imeProjectPath = join(appRoot, 'windows-ime', 'OpenLessIme.vcxproj');
|
|
const imeEditSessionPath = join(appRoot, 'windows-ime', 'src', 'edit_session.cpp');
|
|
const imeTextServicePath = join(appRoot, 'windows-ime', 'src', 'text_service.cpp');
|
|
const tauriConfigPath = join(appRoot, 'src-tauri', 'tauri.conf.json');
|
|
const nsisHookPath = join(appRoot, 'src-tauri', 'nsis', 'openless-ime-hooks.nsh');
|
|
const wixFragmentPath = join(appRoot, 'src-tauri', 'wix', 'openless-ime.wxs');
|
|
|
|
const script = readFileSync(scriptPath, 'utf8');
|
|
const launcher = readFileSync(launcherPath, 'utf8');
|
|
const ciWorkflow = readFileSync(ciWorkflowPath, 'utf8');
|
|
const imeBuild = readFileSync(imeBuildPath, 'utf8');
|
|
const imeInstallSmoke = readFileSync(imeInstallSmokePath, 'utf8');
|
|
const realAsrInsertionSmoke = readFileSync(realAsrInsertionSmokePath, 'utf8');
|
|
const imeRegister = readFileSync(imeRegisterPath, 'utf8');
|
|
const imeUnregister = readFileSync(imeUnregisterPath, 'utf8');
|
|
const imeSolution = readFileSync(imeSolutionPath, 'utf8');
|
|
const imeProject = readFileSync(imeProjectPath, 'utf8');
|
|
const imeEditSession = readFileSync(imeEditSessionPath, 'utf8');
|
|
const imeTextService = readFileSync(imeTextServicePath, 'utf8');
|
|
const tauriConfig = JSON.parse(readFileSync(tauriConfigPath, 'utf8'));
|
|
const nsisHook = readFileSync(nsisHookPath, 'utf8');
|
|
const wixFragment = readFileSync(wixFragmentPath, 'utf8');
|
|
|
|
const requiredFragments = [
|
|
'Install-RustMsvcToolchain',
|
|
'https://win.rustup.rs/x86_64',
|
|
'stable-x86_64-pc-windows-msvc',
|
|
'Find-VsDevCmd',
|
|
'VsDevCmd.bat',
|
|
'npm.cmd ci',
|
|
'windows-ime-build.ps1',
|
|
'OPENLESS_IME_DLL_X64',
|
|
'OPENLESS_IME_DLL_X86',
|
|
'OpenLessIme.dll',
|
|
'tauri build -- --target x86_64-pc-windows-msvc --bundles msi',
|
|
'Repair-TauriMsiBundle',
|
|
'light.exe',
|
|
'main.wixobj',
|
|
'openless-ime.wixobj',
|
|
'locale.wxl',
|
|
'WebView2Loader.dll',
|
|
'Compress-Archive',
|
|
'Get-FileHash -Algorithm SHA256',
|
|
];
|
|
|
|
for (const fragment of requiredFragments) {
|
|
assert.match(
|
|
script,
|
|
new RegExp(fragment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
|
|
`missing ${fragment}`,
|
|
);
|
|
}
|
|
|
|
assert.match(
|
|
script,
|
|
/\[switch\]\$SkipRustInstall/,
|
|
'script should support opting out of Rust installation',
|
|
);
|
|
assert.match(
|
|
script,
|
|
/\[switch\]\$SkipNpmCi/,
|
|
'script should support reusing existing node_modules',
|
|
);
|
|
assert.match(
|
|
script,
|
|
/\[switch\]\$CleanArtifacts/,
|
|
'script should support cleaning the output directory',
|
|
);
|
|
assert.doesNotMatch(
|
|
script,
|
|
/WixTools314/,
|
|
'MSVC packaging must not hard-code a single Tauri WiX tools version',
|
|
);
|
|
assert.doesNotMatch(
|
|
ciWorkflow,
|
|
/WixTools314/,
|
|
'CI MSI repair must not hard-code a single Tauri WiX tools version',
|
|
);
|
|
assert.match(
|
|
script,
|
|
/cmd\.exe \/V:ON \/d \/c/,
|
|
'MSVC packaging must preserve the VsDevCmd environment with delayed expansion',
|
|
);
|
|
assert.match(
|
|
script,
|
|
/PATH=\$CargoBin;!PATH!/,
|
|
'MSVC packaging must expand PATH after VsDevCmd runs',
|
|
);
|
|
assert.doesNotMatch(
|
|
script,
|
|
/PATH=\$CargoBin;%PATH%/,
|
|
'MSVC packaging must not eagerly expand PATH before VsDevCmd',
|
|
);
|
|
assert.match(
|
|
script,
|
|
/-Filter "WixTools\*"/,
|
|
'MSVC packaging should discover Tauri WiX tools by WixTools* glob',
|
|
);
|
|
assert.match(
|
|
ciWorkflow,
|
|
/WixTools\*\\light\.exe/,
|
|
'CI MSI repair should discover Tauri WiX tools by WixTools* glob',
|
|
);
|
|
|
|
assert.match(
|
|
imeBuild,
|
|
/\[string\]\$OutputDirectory/,
|
|
'IME build should support a package-specific output directory',
|
|
);
|
|
assert.match(
|
|
imeBuild,
|
|
/\[string\]\$IntermediateDirectory/,
|
|
'IME build should support a package-specific intermediate directory',
|
|
);
|
|
assert.match(
|
|
imeBuild,
|
|
/\[ValidateSet\("x64", "Win32"\)\]/,
|
|
'IME build should support x64 and Win32 platforms',
|
|
);
|
|
assert.match(imeBuild, /\/p:Platform=\$Platform/, 'IME build should pass Platform to MSBuild');
|
|
assert.match(
|
|
imeBuild,
|
|
/\$defaultOutputDirectory = Join-Path \$appRoot "windows-ime\\\$defaultPlatformFolder\\\$Configuration"/,
|
|
'IME build should force stable default OutDir per platform',
|
|
);
|
|
assert.match(imeBuild, /\/p:OutDir=/, 'IME build should pass OutDir to MSBuild');
|
|
assert.match(imeBuild, /\/p:IntDir=/, 'IME build should pass IntDir to MSBuild');
|
|
assert.match(imeRegister, /windows-ime-build\.ps1/, 'IME register should build before registering');
|
|
assert.doesNotMatch(
|
|
imeRegister,
|
|
/if \(-not \(Test-Path \$dll\)\)/,
|
|
'IME register must rebuild stale DLLs, not only missing DLLs',
|
|
);
|
|
assert.match(
|
|
imeRegister,
|
|
/windows-ime-register/,
|
|
'IME register should use a side-by-side staging output to avoid locked registered DLLs',
|
|
);
|
|
assert.match(
|
|
imeRegister,
|
|
/Get-Date/,
|
|
'IME register should create a fresh staging output for each registration run',
|
|
);
|
|
assert.match(
|
|
imeRegister,
|
|
/\$PID/,
|
|
'IME register should include the process id in the staging output to avoid path reuse',
|
|
);
|
|
assert.match(
|
|
imeRegister,
|
|
/-OutputDirectory/,
|
|
'IME register should pass a staging output directory to the build script',
|
|
);
|
|
assert.match(
|
|
imeRegister,
|
|
/-IntermediateDirectory/,
|
|
'IME register should pass a staging intermediate directory to the build script',
|
|
);
|
|
assert.match(
|
|
imeRegister,
|
|
/active-registration\.json/,
|
|
'IME register should persist the staged DLL paths it registered',
|
|
);
|
|
assert.match(
|
|
imeUnregister,
|
|
/active-registration\.json/,
|
|
'IME unregister should read the registered staged DLL manifest',
|
|
);
|
|
assert.match(
|
|
imeUnregister,
|
|
/windows-ime-register/,
|
|
'IME unregister should target the same staging root used by register',
|
|
);
|
|
assert.match(
|
|
imeUnregister,
|
|
/ConvertFrom-Json/,
|
|
'IME unregister should parse persisted registered DLL paths',
|
|
);
|
|
assert.doesNotMatch(
|
|
imeUnregister,
|
|
/windows-ime\\\$folder\\\$Configuration\\OpenLessIme\.dll/,
|
|
'IME unregister must not only derive legacy build-output DLL paths',
|
|
);
|
|
|
|
assert.deepEqual(tauriConfig.bundle.windows.wix.fragmentPaths, ['wix/openless-ime.wxs']);
|
|
assert.deepEqual(tauriConfig.bundle.windows.wix.componentRefs, [
|
|
'OpenLessImeDllX64Component',
|
|
'OpenLessImeDllX86Component',
|
|
]);
|
|
assert.equal(
|
|
tauriConfig.bundle.windows.nsis.installMode,
|
|
'perMachine',
|
|
'NSIS must force a machine-wide install because TSF registration is machine-wide',
|
|
);
|
|
assert.equal(
|
|
tauriConfig.bundle.windows.nsis.installerHooks,
|
|
'nsis/openless-ime-hooks.nsh',
|
|
'NSIS must install and register the TSF DLLs',
|
|
);
|
|
|
|
assert.match(
|
|
imeSolution,
|
|
/Release\|Win32/,
|
|
'IME solution should include a Win32 Release configuration',
|
|
);
|
|
assert.match(
|
|
imeProject,
|
|
/Release\|Win32/,
|
|
'IME project should include a Win32 Release configuration',
|
|
);
|
|
assert.match(
|
|
imeProject,
|
|
/<AdditionalOptions>\/utf-8 %\(AdditionalOptions\)<\/AdditionalOptions>/,
|
|
'IME project must decode UTF-8 source independently of the runner code page',
|
|
);
|
|
assert.match(
|
|
imeTextService,
|
|
/TF_E_SYNCHRONOUS/,
|
|
'IME should detect hosts like Word that reject synchronous edit sessions',
|
|
);
|
|
assert.match(
|
|
imeTextService,
|
|
/TF_ES_ASYNC \| TF_ES_READWRITE/,
|
|
'IME should retry Word-hosted commits with an async edit session',
|
|
);
|
|
assert.match(
|
|
imeTextService,
|
|
/WaitForSingleObject/,
|
|
'IME pipe submit should wait for async edit-session completion',
|
|
);
|
|
assert.match(
|
|
imeEditSession,
|
|
/SetEvent/,
|
|
'IME edit session should signal async completion back to the pipe submitter',
|
|
);
|
|
assert.match(
|
|
imeEditSession,
|
|
/Collapse\(edit_cookie, TF_ANCHOR_END\)/,
|
|
'IME should collapse the committed range to its end after insertion',
|
|
);
|
|
assert.match(
|
|
imeEditSession,
|
|
/SetSelection\(edit_cookie, 1, &selection\)/,
|
|
'IME should move the caret to the end of inserted text',
|
|
);
|
|
assert.match(
|
|
imeEditSession,
|
|
/TF_AE_END/,
|
|
'IME should make the end of the committed text the active selection end',
|
|
);
|
|
const insertAtSelectionCalls = imeEditSession.match(/\bInsertTextAtSelection\s*\(/g) ?? [];
|
|
assert.equal(
|
|
insertAtSelectionCalls.length,
|
|
1,
|
|
'IME should submit the dictated text to the host exactly once',
|
|
);
|
|
assert.doesNotMatch(
|
|
imeEditSession,
|
|
/TF_IAS_QUERYONLY/,
|
|
'IME should not preflight the full text through host text stores before committing it',
|
|
);
|
|
|
|
assert.match(
|
|
wixFragment,
|
|
/DirectoryRef Id="INSTALLDIR"/,
|
|
'WiX fragment should install into the app directory',
|
|
);
|
|
assert.match(
|
|
wixFragment,
|
|
/Component Id="OpenLessImeDllX64Component"/,
|
|
'WiX fragment should define the x64 TSF DLL component',
|
|
);
|
|
assert.match(
|
|
wixFragment,
|
|
/Component Id="OpenLessImeDllX86Component"/,
|
|
'WiX fragment should define the x86 TSF DLL component',
|
|
);
|
|
assert.match(
|
|
wixFragment,
|
|
/Source="src-tauri\\target\\windows-ime-msvc\\x64\\Release\\OpenLessIme\.dll"/,
|
|
'WiX fragment should consume the package-built x64 IME DLL',
|
|
);
|
|
assert.match(
|
|
wixFragment,
|
|
/Source="src-tauri\\target\\windows-ime-msvc\\x86\\Release\\OpenLessIme\.dll"/,
|
|
'WiX fragment should consume the package-built x86 IME DLL',
|
|
);
|
|
assert.match(wixFragment, /regsvr32\.exe/, 'MSI should register and unregister the TSF DLL');
|
|
assert.match(
|
|
wixFragment,
|
|
/\[System64Folder\]regsvr32\.exe/,
|
|
'MSI should register the x64 IME with 64-bit regsvr32',
|
|
);
|
|
assert.match(
|
|
wixFragment,
|
|
/\[WindowsFolder\]SysWOW64\\regsvr32\.exe/,
|
|
'MSI should register the x86 IME with 32-bit regsvr32',
|
|
);
|
|
assert.match(
|
|
wixFragment,
|
|
/RegisterOpenLessImeX64/,
|
|
'MSI should register x64 OpenLess IME during install',
|
|
);
|
|
assert.match(
|
|
wixFragment,
|
|
/RegisterOpenLessImeX86/,
|
|
'MSI should register x86 OpenLess IME during install',
|
|
);
|
|
assert.match(
|
|
wixFragment,
|
|
/UnregisterOpenLessImeX64/,
|
|
'MSI should unregister x64 OpenLess IME during uninstall',
|
|
);
|
|
assert.match(
|
|
wixFragment,
|
|
/UnregisterOpenLessImeX86/,
|
|
'MSI should unregister x86 OpenLess IME during uninstall',
|
|
);
|
|
|
|
assert.match(
|
|
nsisHook,
|
|
/NSIS_HOOK_PREINSTALL/,
|
|
'NSIS should copy IME DLLs before install completes',
|
|
);
|
|
assert.match(
|
|
nsisHook,
|
|
/NSIS_HOOK_POSTINSTALL/,
|
|
'NSIS should register IME DLLs after files are installed',
|
|
);
|
|
assert.match(
|
|
nsisHook,
|
|
/NSIS_HOOK_PREUNINSTALL/,
|
|
'NSIS should unregister IME DLLs before uninstall removes them',
|
|
);
|
|
assert.match(
|
|
nsisHook,
|
|
/OPENLESS_IME_STAGE_AND_REPLACE "x64" "OPENLESS_IME_DLL_X64"/,
|
|
'NSIS should consume the CI-built x64 IME DLL',
|
|
);
|
|
assert.match(
|
|
nsisHook,
|
|
/OPENLESS_IME_STAGE_AND_REPLACE "x86" "OPENLESS_IME_DLL_X86"/,
|
|
'NSIS should consume the CI-built x86 IME DLL',
|
|
);
|
|
assert.match(
|
|
nsisHook,
|
|
/SetOutPath "\$INSTDIR\\windows-ime\\\$\{PLATFORM_DIR\}"/,
|
|
'NSIS should install the IME DLL beside the app by platform',
|
|
);
|
|
assert.match(
|
|
nsisHook,
|
|
/File \/oname=OpenLessIme\.dll\.new "\$%\$\{ENV_VAR\}%"/,
|
|
'NSIS should embed OpenLessIme.dll in the installer',
|
|
);
|
|
assert.match(
|
|
nsisHook,
|
|
/Sysnative\\regsvr32\.exe/,
|
|
'NSIS should use 64-bit regsvr32 for the x64 IME',
|
|
);
|
|
assert.match(
|
|
nsisHook,
|
|
/SysWOW64\\regsvr32\.exe/,
|
|
'NSIS should use 32-bit regsvr32 for the x86 IME',
|
|
);
|
|
assert.match(
|
|
nsisHook,
|
|
/System32\\regsvr32\.exe[\s\S]*windows-ime\\x86\\OpenLessIme\.dll/,
|
|
'NSIS should use System32 regsvr32 for the x86 IME on 32-bit Windows',
|
|
);
|
|
assert.match(nsisHook, /Abort/, 'NSIS install should fail if TSF registration fails');
|
|
assert.match(
|
|
nsisHook,
|
|
/OPENLESS_IME_ABORT_IF_FAILED \$0 "x64 registration"/,
|
|
'NSIS install should fail if x64 TSF registration fails',
|
|
);
|
|
assert.match(
|
|
nsisHook,
|
|
/OPENLESS_IME_ABORT_IF_FAILED \$0 "x86 registration"/,
|
|
'NSIS install should fail if x86 TSF registration fails',
|
|
);
|
|
assert.match(
|
|
nsisHook,
|
|
/OPENLESS_IME_REGISTER_X86[\s\S]*\$\{If\} \$0 != 0[\s\S]*StrCpy \$1 \$0[\s\S]*OPENLESS_IME_UNREGISTER_X64[\s\S]*StrCpy \$0 \$1[\s\S]*OPENLESS_IME_ABORT_IF_FAILED \$0 "x86 registration"/,
|
|
'NSIS install should roll back x64 registration before aborting on x86 registration failure',
|
|
);
|
|
assert.doesNotMatch(
|
|
nsisHook,
|
|
/OPENLESS_IME_ABORT_IF_FAILED \$0 "x64 unregistration"/,
|
|
'NSIS uninstall should not fail if x64 TSF unregistration fails',
|
|
);
|
|
assert.doesNotMatch(
|
|
nsisHook,
|
|
/OPENLESS_IME_ABORT_IF_FAILED \$0 "x86 unregistration"/,
|
|
'NSIS uninstall should not fail if x86 TSF unregistration fails',
|
|
);
|
|
assert.match(
|
|
nsisHook,
|
|
/OpenLess x64 TSF IME unregister exit code \$0/,
|
|
'NSIS uninstall should log x64 TSF unregistration failures',
|
|
);
|
|
assert.match(
|
|
nsisHook,
|
|
/OpenLess x86 TSF IME unregister exit code \$0/,
|
|
'NSIS uninstall should log x86 TSF unregistration failures',
|
|
);
|
|
|
|
assert.match(
|
|
imeInstallSmoke,
|
|
/\[ValidateSet\("nsis", "msi"\)\]/,
|
|
'install smoke should support both Windows installers',
|
|
);
|
|
assert.match(
|
|
imeInstallSmoke,
|
|
/Join-ProcessArguments/,
|
|
'install smoke should quote process arguments before Start-Process',
|
|
);
|
|
assert.match(
|
|
imeInstallSmoke,
|
|
/\$commandLine = Join-ProcessArguments \$ArgumentList/,
|
|
'install smoke should build a single quoted command line',
|
|
);
|
|
assert.match(
|
|
imeInstallSmoke,
|
|
/Start-Process -FilePath \$FilePath -ArgumentList \$commandLine/,
|
|
'install smoke should pass a single quoted command line to Start-Process',
|
|
);
|
|
assert.match(
|
|
imeInstallSmoke,
|
|
/OpenLessImeSubmit/,
|
|
'install smoke should preserve TSF backend context',
|
|
);
|
|
assert.match(
|
|
realAsrInsertionSmoke,
|
|
/\$targetTextForComparison -cne \$finalTextForComparison/,
|
|
'real insertion smoke should reject duplicate or partial text in initially empty targets',
|
|
);
|
|
assert.match(
|
|
imeInstallSmoke,
|
|
/Software\\Classes\\CLSID\\\{6B9F3F4F-5EE7-42D6-9C61-9F80B03A5D7D\}\\InprocServer32/,
|
|
'install smoke should check x64 COM registration',
|
|
);
|
|
assert.match(
|
|
imeInstallSmoke,
|
|
/Software\\WOW6432Node\\Classes\\CLSID\\\{6B9F3F4F-5EE7-42D6-9C61-9F80B03A5D7D\}\\InprocServer32/,
|
|
'install smoke should check x86 COM registration',
|
|
);
|
|
assert.match(
|
|
imeInstallSmoke,
|
|
/LanguageProfile\\0x00000804\\\{9B5F5E04-23F6-47DA-9A26-D221F6C3F02E\}/,
|
|
'install smoke should check the TSF language profile',
|
|
);
|
|
assert.match(
|
|
imeInstallSmoke,
|
|
/Category\\Category\\\{34745C63-B2F0-4784-8B67-5E12C8701A31\}/,
|
|
'install smoke should check the keyboard TSF category',
|
|
);
|
|
assert.match(
|
|
imeInstallSmoke,
|
|
/foreach \(\$key in \$ExpectedBackendKeys\) \{[\s\S]*Assert-RegistryKey -View Registry64 -SubKey \$key[\s\S]*\}/,
|
|
'install smoke should assert every backend-required registry key exists',
|
|
);
|
|
assert.doesNotMatch(
|
|
imeInstallSmoke,
|
|
/foreach \(\$key in \$ExpectedBackendKeys\) \{[\s\S]*Write-Host "\[trace\] backend-required key: HKLM\\\$key"[\s\S]*\}/,
|
|
'install smoke must not only trace backend-required registry keys',
|
|
);
|
|
assert.match(
|
|
ciWorkflow,
|
|
/windows-ime-install-smoke\.ps1[\s\S]*-InstallerKind nsis/,
|
|
'CI should install and verify the NSIS artifact',
|
|
);
|
|
assert.match(
|
|
ciWorkflow,
|
|
/windows-ime-install-smoke\.ps1[\s\S]*-InstallerKind msi/,
|
|
'CI should install and verify the MSI artifact',
|
|
);
|
|
assert.match(
|
|
ciWorkflow,
|
|
/InstallerKind nsis[\s\S]*\$LASTEXITCODE -ne 0[\s\S]*NSIS installer smoke failed/,
|
|
'CI should fail immediately when the NSIS smoke run fails',
|
|
);
|
|
assert.match(
|
|
ciWorkflow,
|
|
/InstallerKind msi[\s\S]*\$LASTEXITCODE -ne 0[\s\S]*MSI installer smoke failed/,
|
|
'CI should fail when the MSI smoke run fails',
|
|
);
|
|
|
|
assert.match(launcher, /powershell\.exe/, 'launcher should call powershell.exe');
|
|
assert.match(
|
|
launcher,
|
|
/-ExecutionPolicy Bypass/,
|
|
'launcher should bypass execution policy for this process',
|
|
);
|
|
assert.match(launcher, /windows-package-msvc\.ps1/, 'launcher should invoke the packaging script');
|
|
assert.match(launcher, /%SUPPLIED_ARGS%/, 'launcher should forward user arguments');
|