Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
6252f7a
Fix Windows notifications and multi-account startup
matheuscardosoj Sep 5, 2026
8ca51d6
Merge remote-tracking branch 'origin/master' into fix/windows-notific…
matheuscardosoj Oct 16, 2026
40b479f
Fix Windows build and account notifications
matheuscardosoj Oct 16, 2026
bfb489b
Fix account switcher layout and package Windows installer
matheuscardosoj Oct 16, 2026
b750539
Improve Windows workspace and notification reliability
matheuscardosoj Oct 20, 2026
4825101
Fix Windows resize deadlock and first-login workspace
matheuscardosoj Oct 20, 2026
d6bdb94
Fit controls into WhatsApp header and align app styling
matheuscardosoj Oct 20, 2026
e9eeaeb
Avoid repeated mounting when header space is insufficient
matheuscardosoj Oct 20, 2026
afad60a
Style recovery toolbar and remove its compact scrollbar
matheuscardosoj Oct 20, 2026
31b0aa0
Fix account activation, anchored menus and lock lifecycle; refine Por…
matheuscardosoj Oct 20, 2026
03d308f
Fix Windows notification constructor routing and minimized-window del…
matheuscardosoj Oct 20, 2026
a628ff8
Synchronize child WebView visibility with minimized and hidden accoun…
matheuscardosoj Oct 20, 2026
b635816
Make hide-lock silent, cancel locked notifications and harden UI life…
matheuscardosoj Oct 20, 2026
0de71bd
Keep unit fixture logs separate from installed app diagnostics
matheuscardosoj Oct 20, 2026
0f04f2e
Prevent native callback and reparent layout deadlocks; coalesce geome…
matheuscardosoj Oct 20, 2026
56dd1c7
Preserve main account when closing detached window
matheuscardosoj Oct 20, 2026
1533d96
Persist config JSON atomically and fail closed on a corrupt app lock
matheuscardosoj Oct 22, 2026
ed63419
Harden unlock, downloads and webview permissions; tighten CI
matheuscardosoj Oct 22, 2026
ba796fe
Add interface language setting (Português / English)
matheuscardosoj Oct 22, 2026
c624bf7
Release 0.6.9: document the language setting
matheuscardosoj Oct 22, 2026
966cb00
Merge upstream master (menu-bar-only mode, macOS native file drop)
matheuscardosoj Oct 22, 2026
0e98ec5
Add the missing i18n files; fix non-Windows clippy errors
matheuscardosoj Oct 22, 2026
c0c8d66
Fix macOS type error and Linux dead-code warnings in CI
matheuscardosoj Oct 22, 2026
f0e197f
Fix cross-platform CI warnings and capability test
matheuscardosoj Oct 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- platform: macos-latest
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4

- name: Install Linux deps
if: matrix.platform == 'ubuntu-22.04'
Expand All @@ -27,12 +27,21 @@ jobs:
sudo apt install -y libwebkit2gtk-4.1-dev build-essential curl wget file \
libxdo-dev libdbus-1-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev libhunspell-dev patchelf

- uses: dtolnay/rust-toolchain@stable
- uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec # stable

- uses: swatinem/rust-cache@v2
- uses: swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2
with:
workspaces: "./src-tauri -> target"

- name: cargo fmt
if: matrix.platform == 'ubuntu-22.04'
working-directory: src-tauri
run: cargo fmt --check

- name: cargo clippy
working-directory: src-tauri
run: cargo clippy --locked -- -D warnings

- name: cargo build (compile check)
working-directory: src-tauri
run: cargo build --locked
Expand All @@ -45,7 +54,7 @@ jobs:
working-directory: src-tauri
run: cargo test --locked

- uses: actions/setup-node@v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 20

Expand All @@ -60,3 +69,9 @@ jobs:
# and shows up only as a dead button on macOS, so it needs a test.
- name: settings-ui dialog tests
run: node settings-ui/dialog.test.mjs

- name: settings-ui language tests (every UI string has an English entry)
run: node settings-ui/i18n.test.mjs

- name: notification tests
run: node src-tauri/resources/notifications.test.mjs
10 changes: 5 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
outputs:
tag: ${{ steps.tag.outputs.tag }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- name: Resolve the release tag
id: tag
env:
Expand Down Expand Up @@ -59,7 +59,7 @@ jobs:
- platform: macos-latest
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4

- name: Install Linux deps
if: matrix.platform == 'ubuntu-22.04'
Expand All @@ -69,9 +69,9 @@ jobs:
libxdo-dev libdbus-1-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev libhunspell-dev patchelf \
desktop-file-utils

- uses: dtolnay/rust-toolchain@stable
- uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec # stable

- uses: swatinem/rust-cache@v2
- uses: swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2
with:
workspaces: "./src-tauri -> target"

Expand All @@ -88,7 +88,7 @@ jobs:
echo "WHATRUST_NOTES_EOF"
} >> "$GITHUB_OUTPUT"

- uses: tauri-apps/tauri-action@v0
- uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 # v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/smoke-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ jobs:
shell: pwsh
run: Get-Content "$env:LOCALAPPDATA\whatRust\logs\whatrust.log"

- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: always()
with:
name: smoke-windows-screenshots
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,7 @@ dist/
/builddir/
/repo/
/*.flatpak

# UI validation artifacts
/.playwright-cli/
/output/
128 changes: 128 additions & 0 deletions docs/windows-usabilidade.md

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,19 @@ $out = Join-Path $env:TEMP $asset.name
Write-Host "Downloading $($asset.name)..."
Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $out -UseBasicParsing

# GitHub publishes a SHA-256 digest for each release asset; refuse a mismatch.
$expected = if ($asset.digest -match '^sha256:(?<h>[0-9a-fA-F]{64})$') { $Matches.h } else { $null }
$actual = (Get-FileHash -Algorithm SHA256 -Path $out).Hash
if ($expected) {
if ($actual -ne $expected) {
Remove-Item -Force $out
throw "Checksum mismatch for $($asset.name): expected $expected, got $actual. Aborting."
}
Write-Host "SHA-256 verified: $actual"
} else {
Write-Warning "GitHub published no digest for this asset; SHA-256 is $actual. Compare it with the release notes before trusting the installer."
}

Write-Host "Launching installer..."
if ($asset.name -match '\.msi$') {
Start-Process msiexec.exe -ArgumentList "/i `"$out`"" -Wait
Expand Down
75 changes: 75 additions & 0 deletions scripts/adapter-check.template.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
async (page) => {
const checks=[];
function check(name,pass){if(!pass)throw Error(name);checks.push(name);}
await page.clock.install();
await page.addInitScript(() => {
window.__WHATRUST_WINDOWS__=true;window.calls=[];
window.__TAURI__={core:{invoke:async(cmd,args)=>{window.calls.push({cmd,args});if(cmd==='integration_state')return {accounts:[{id:'default',name:'Matheus',unread:2}],current_id:'default',native_notifications:true};return null;}}};
});
await page.route('https://web.whatsapp.com/**',route=>route.fulfill({contentType:'text/html',body:'<html><meta charset="utf-8"><style>body{margin:0;background:#161717;color:#eee;font:14px system-ui}#app>header{display:flex;align-items:center;height:64px;padding:0 24px;gap:16px}h1{font-size:22px;flex:1}#main{padding:24px}</style><div id="app"><header><h1>WhatsApp</h1><button aria-label="Menu">⋮</button></header><div id="main"><header data-muted="false"></header><div id="messages"><div data-id="false_chatA_history">Histórico</div></div></div></div></html>'}));
await page.goto('https://web.whatsapp.com/');
await page.addScriptTag({content:__ADAPTER_SOURCE__});
await page.clock.runFor(3000);
const signals=()=>page.evaluate(()=>window.calls.filter(c=>c.cmd==='report_message_signal').map(c=>c.args.signal));
check('initial_history_is_silent',(await signals()).length===0);
async function append(id){await page.evaluate(id=>{const r=document.createElement('div');r.dataset.id=id;r.textContent='Mesmo texto';document.getElementById('messages').appendChild(r);},id);await page.clock.runFor(300);await page.waitForTimeout(20);}
await append('false_chatA_new1');await append('false_chatA_new2');
let s=await signals();check('identical_text_distinct_messages',s.length===2&&s[0].id!==s[1].id);
check('identifier_is_hashed',s.every(x=>/^[a-f0-9]{64}$/.test(x.id)));
check('explicit_unmuted_state',s.every(x=>x.muted===false));
await append('true_chatA_outgoing');check('outgoing_is_silent',(await signals()).length===2);
await page.evaluate(()=>document.querySelector('#main header').dataset.muted='true');await append('false_chatA_muted');
check('muted_state_reported',(await signals()).at(-1).muted===true);
await page.evaluate(()=>document.querySelector('#main header').removeAttribute('data-muted'));await append('false_chatA_unknown');
check('unknown_mute_state_reported',(await signals()).at(-1).muted===null);
let count=(await signals()).length;
await page.evaluate(()=>{const r=document.createElement('div');r.dataset.id='false_chatA_older';document.getElementById('messages').prepend(r);});await page.clock.runFor(300);
check('scroll_history_is_silent',(await signals()).length===count);
await page.evaluate(()=>document.getElementById('messages').innerHTML='<div data-id="false_chatB_history">Outra conversa</div>');await page.clock.runFor(300);
check('conversation_change_is_silent',(await signals()).length===count);
await page.getByRole('button',{name:'Configurações do whatRust'}).click();
check('settings_uses_limited_opener',await page.evaluate(()=>window.calls.some(c=>c.cmd==='open_app_panel')));
await page.evaluate(()=>document.querySelector('#app>header').remove());await page.clock.runFor(300);
check('missing_header_activates_recovery',await page.evaluate(()=>window.calls.some(c=>c.cmd==='report_integration'&&c.args.mounted===false)));
await page.evaluate(()=>{const h=document.createElement('header');h.innerHTML='<h1>WhatsApp</h1>';document.getElementById('app').prepend(h);});await page.clock.runFor(300);
check('header_replacement_remounts_once',await page.locator('#__whatrust_controls').count()===1);
await page.getByRole('button',{name:'Trocar conta: Matheus · 2 conversas não lidas',exact:true}).click();
await page.evaluate(()=>window.__whatrustState({accounts:[{id:'default',name:'Matheus',unread:3}],current_id:'default',native_notifications:true}));
check('live_unread_update_preserves_menu_focus',await page.locator('#__whatrust_controls').evaluate(host=>host.shadowRoot.activeElement?.dataset.item==='default'));
await page.keyboard.press('Escape');
check('escape_restores_focus',await page.locator('#__whatrust_controls').evaluate(host=>host.shadowRoot.activeElement?.title==='Conta: Matheus — Trocar conta'));
for(const width of [560,900,1920]){await page.setViewportSize({width,height:800});const box=await page.locator('#__whatrust_controls').boundingBox();check('header_fits_'+width,box.x>=0&&box.x+box.width<=width);}
// Regression from the real WhatsApp DOM: the 64px navigation rail is
// itself a header, before two nested list headers and a full-width action row.
await page.evaluate(()=>{
document.body.innerHTML=`<style>
#app{display:flex;width:100vw;height:100vh}#app>#rail{box-sizing:border-box;width:64px;height:100vh;display:flex;align-items:center;flex-direction:column;flex:none}
#list{width:clamp(340px,30vw,768px);flex:none}#outer,#inner{width:100%;height:64px;box-sizing:border-box}
#inner{display:flex;align-items:center;padding:0 20px}#actions{display:flex;align-items:center;width:100%;height:40px}
#actions h1{font:700 20px system-ui;flex:1;margin:0}#native{display:flex;flex:none}#native button{width:40px;height:40px}
#side{width:100%;height:600px}#main{flex:1;padding:0}
</style><div id="app"><header id="rail"><button>Chats</button><button>Status</button></header><section id="list"><header id="outer"><header id="inner"><div id="actions"><h1>WhatsApp</h1><span id="native"><button aria-label="Menu original">⋮</button><button aria-label="Nova conversa">+</button></span></div></header></header><div id="side"></div></section><div id="main"><header style="height:64px">Conversa</header></div></div>`;
});await page.clock.runFor(300);
check('rejects_vertical_navigation_header',await page.locator('#actions>#__whatrust_controls').count()===1);
for(const width of [560,900,1920]){
await page.setViewportSize({width,height:800});await page.waitForTimeout(30);await page.clock.runFor(300);
const fit=await page.locator('#__whatrust_controls').evaluate(host=>{
const r=host.getBoundingClientRect(),header=host.closest('header').getBoundingClientRect();
const buttons=Array.from(host.shadowRoot.querySelectorAll('.bar>button')).map(e=>e.getBoundingClientRect());
const native=Array.from(document.querySelectorAll('#native button')).map(e=>e.getBoundingClientRect());
return r.left>=header.left&&r.right<=header.right+1&&r.bottom<=header.bottom+1&&buttons.every(b=>b.width>=40&&b.height>=40)&&native.every(b=>b.right<=r.left||b.left>=r.right);
});if(!fit)throw Error('header geometry '+JSON.stringify(await page.locator('#__whatrust_controls').evaluate(h=>({host:h.getBoundingClientRect().toJSON(),head:h.closest('header').getBoundingClientRect().toJSON(),buttons:Array.from(h.shadowRoot.querySelectorAll('.bar>button')).map(e=>e.getBoundingClientRect().toJSON()),native:Array.from(document.querySelectorAll('#native button')).map(e=>e.getBoundingClientRect().toJSON())}))));check('realistic_header_controls_fit_'+width,fit);
}
await page.evaluate(()=>document.getElementById('list').style.width='180px');await page.waitForTimeout(40);await page.clock.runFor(300);
check('unsafe_narrow_header_uses_recovery',await page.locator('#__whatrust_controls').count()===0&&await page.evaluate(()=>window.calls.filter(c=>c.cmd==='report_integration').at(-1).args.mounted===false));
await page.evaluate(()=>document.getElementById('list').style.width='');await page.waitForTimeout(40);await page.clock.runFor(300);
check('safe_width_restores_controls_once',await page.locator('#actions>#__whatrust_controls').count()===1);
await page.evaluate(()=>{window.__mountMutations=0;window.__probe=new MutationObserver(rs=>{for(const r of rs)for(const n of [...r.addedNodes,...r.removedNodes])if(n.id==='__whatrust_controls')window.__mountMutations++;});window.__probe.observe(document.getElementById('app'),{subtree:true,childList:true});document.getElementById('list').style.width='280px';});
await page.waitForTimeout(40);await page.clock.runFor(1600);
check('unsafe_header_does_not_retry_own_mutations',await page.evaluate(()=>window.__mountMutations<=4));
await page.evaluate(()=>{window.__probe.disconnect();document.getElementById('list').style.width='';});await page.waitForTimeout(40);await page.clock.runFor(300);
check('reports_whatsapp_dark_theme',await page.evaluate(()=>window.calls.filter(c=>c.cmd==='report_integration').at(-1).args.theme==='dark'));
await page.evaluate(()=>document.body.style.color='rgb(10,10,10)');await page.clock.runFor(300);
check('reports_whatsapp_light_theme_change',await page.evaluate(()=>window.calls.filter(c=>c.cmd==='report_integration').at(-1).args.theme==='light'));
return {passed:true,checks};
}
40 changes: 40 additions & 0 deletions scripts/shell-check.template.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
async(page)=>{
const checks=[];const check=(n,p)=>{if(!p)throw Error(n);checks.push(n)};
await page.addInitScript(()=>{window.calls=[];window.__TAURI__={core:{invoke:async(cmd,args)=>{window.calls.push({cmd,args});return cmd==='list_accounts'?[{id:'default',name:'Matheus',unread:2},{id:'acct-1',name:'VS Prestadora de Serviços',unread:0}]:null;}}};});
await page.emulateMedia({colorScheme:'dark'});
await page.goto(__SHELL_URL__);
for(const width of [480,560,1920]){
await page.setViewportSize({width,height:48});
check('compact_shell_no_scroll_'+width,await page.evaluate(()=>document.documentElement.scrollHeight<=innerHeight&&document.documentElement.scrollWidth<=innerWidth&&getComputedStyle(document.querySelector('main')).display==='none'));
check('toolbar_controls_fit_'+width,await page.evaluate(()=>Array.from(document.querySelectorAll('button,select')).every(e=>{const r=e.getBoundingClientRect();return r.left>=0&&r.right<=innerWidth&&r.top>=0&&r.bottom<=innerHeight;})));
}
await page.selectOption('#accounts','acct-1');
check('account_switch_preserved',await page.evaluate(()=>window.calls.some(c=>c.cmd==='open_account'&&c.args.id==='acct-1')));
await page.getByRole('button',{name:'Configurações do whatRust',exact:true}).click();
check('settings_access_preserved',await page.evaluate(()=>window.calls.some(c=>c.cmd==='open_app_panel')));
await page.setViewportSize({width:1000,height:800});
check('full_window_empty_state',await page.getByRole('heading',{name:'Escolha uma conta para continuar'}).isVisible());
check('neutral_dark_background',await page.evaluate(()=>getComputedStyle(document.body).backgroundColor==='rgb(22, 23, 23)'));
await page.setViewportSize({width:1280,height:48});

await page.evaluate(()=>window.refresh('default'));
check('arrow_close_to_short_name',await page.evaluate(()=>{const m=document.getElementById('account-size'),r=document.createRange();r.selectNodeContents(m);const arrow=document.querySelector('.chevron').getBoundingClientRect();return Math.abs(arrow.left-r.getBoundingClientRect().right-8)<2;}));
await page.screenshot({path:'output/playwright/shell-short-local5.png'});
const longName='Conta empresarial com um nome muito longo para conferir as reticências no seletor';
await page.evaluate(name=>{const s=document.getElementById('accounts');s.options[1].textContent=name;s.value='acct-1';s.dispatchEvent(new Event('change'));},longName);
const longWidth=await page.locator('#accounts').evaluate(e=>e.getBoundingClientRect().width);
check('long_name_capped_with_ellipsis',await page.locator('#accounts').evaluate(e=>getComputedStyle(e).textOverflow==='ellipsis'&&e.getBoundingClientRect().width<=320&&document.getElementById('account-size').scrollWidth>e.clientWidth));
check('full_name_available_in_tooltip',await page.locator('#accounts').getAttribute('title')===longName);
await page.screenshot({path:'output/playwright/shell-long-local5.png'});
await page.evaluate(()=>window.refresh('acct-1'));
check('selected_account_tracks_active_window',await page.locator('#accounts').inputValue()==='acct-1');
await page.evaluate(()=>window.refresh('default'));
check('short_name_shrinks_after_long_name',await page.locator('#accounts').evaluate(e=>e.getBoundingClientRect().width)<longWidth);

await page.addInitScript(()=>{window.__TAURI__.core.invoke=(cmd)=>cmd==='list_accounts'?new Promise(resolve=>window.__releaseList=resolve):Promise.resolve();});
await page.reload();
await page.evaluate(async()=>{window.refresh('default');window.refresh('acct-1');window.__releaseList([{id:'default',name:'Matheus'},{id:'acct-1',name:'Empresa'}]);});
await page.waitForFunction(()=>document.getElementById('accounts').options.length===2);
check('delayed_refresh_keeps_latest_active_account',await page.locator('#accounts').inputValue()==='acct-1');
return {passed:true,checks};
}
15 changes: 15 additions & 0 deletions scripts/test-adapter.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
$ErrorActionPreference='Stop'
$taskRoot=Split-Path -Parent $PSScriptRoot
$taskOutput=Join-Path $taskRoot 'output\playwright'
New-Item -ItemType Directory -Path $taskOutput -Force | Out-Null
$taskSource=Get-Content -LiteralPath (Join-Path $taskRoot 'src-tauri\resources\integration.js') -Raw -Encoding UTF8
$taskTemplate=Get-Content -LiteralPath (Join-Path $PSScriptRoot 'adapter-check.template.cjs') -Raw -Encoding UTF8
$taskCode=$taskTemplate.Replace('__ADAPTER_SOURCE__',(ConvertTo-Json -InputObject ([string]$taskSource) -Compress))
$taskFile=Join-Path $taskOutput 'adapter-check.cjs'
[IO.File]::WriteAllText($taskFile,$taskCode,[Text.UTF8Encoding]::new($false))
npx.cmd --yes --package @playwright/cli playwright-cli -s=whatrust-adapter open about:blank | Out-Null
$taskResult=npx.cmd --yes --package @playwright/cli playwright-cli -s=whatrust-adapter run-code --filename $taskFile
$taskResult | Set-Content -LiteralPath (Join-Path $taskOutput 'adapter-result.log') -Encoding UTF8
$taskResult | Select-Object -First 3
npx.cmd --yes --package @playwright/cli playwright-cli -s=whatrust-adapter close | Out-Null
if (($taskResult -join "`n") -match '### Error') { exit 1 }
15 changes: 15 additions & 0 deletions scripts/test-shell.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
$ErrorActionPreference='Stop'
$taskRoot=Split-Path -Parent $PSScriptRoot
$taskOutput=Join-Path $taskRoot 'output\playwright'
New-Item -ItemType Directory -Path $taskOutput -Force | Out-Null
$taskTemplate=Get-Content -LiteralPath (Join-Path $PSScriptRoot 'shell-check.template.cjs') -Raw -Encoding UTF8
$taskUrl=([uri](Join-Path $taskRoot 'settings-ui\shell.html')).AbsoluteUri
$taskCode=$taskTemplate.Replace('__SHELL_URL__',($taskUrl | ConvertTo-Json -Compress))
$taskFile=Join-Path $taskOutput 'shell-check.cjs'
[IO.File]::WriteAllText($taskFile,$taskCode,[Text.UTF8Encoding]::new($false))
npx.cmd --yes --package @playwright/cli playwright-cli -s=whatrust-shell open about:blank | Out-Null
$taskResult=npx.cmd --yes --package @playwright/cli playwright-cli -s=whatrust-shell run-code --filename $taskFile
$taskResult | Set-Content -LiteralPath (Join-Path $taskOutput 'shell-result.log') -Encoding UTF8
$taskResult | Select-Object -First 3
npx.cmd --yes --package @playwright/cli playwright-cli -s=whatrust-shell close | Out-Null
if (($taskResult -join "`n") -match '### Error') { exit 1 }
Loading
Loading