mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-10-18 23:07:27 +02:00
fix(window-state): stop macOS overlay windows changing size on every restore (#3416)
* fix(window-state): convert macOS coordinates to Logical to fix overlay shrink and multi-monitor DPI scaling drift * chore: add changeset for logical coordinates and macOS overlay fix * chore(window-state): fix change file package keys * fix(window-state): save the macOS content rect size instead of guessing the title bar The 20-40px outer/inner heuristic also matched ordinary decorated windows, saving their outer size so they grew by the title bar height on every restore. Derive the title bar height from the content rect instead, in one helper shared by update_state, the Resized handler and the initial metadata. * fix(window-state): keep the saved state in physical pixels State files written by earlier versions hold physical pixels, and logical coordinates are not one global space across monitors with different scale factors, so a position converted with one monitor's scale was restored on another. Converting with the window's scale at Moved time could also pick up a stale scale mid-drag, and an unavailable scale silently stored physical values as logical. * fix(window-state): check the restored position against the monitors For a maximized window the monitor check tested x and y, which are the monitor's corner, while the window was then placed at prev_x and prev_y. A window maximized after leaving a now-disconnected monitor passed the check and was restored off-screen. * test(window-state): cover repeated size and position restores in e2e Restores each window three times, also with an overlay title bar, and checks the inner and outer size (or position) still match the first save, so drift between saving and restoring accumulates and fails the test. * chore(window-state): describe the restore fixes in the change file * chore(deps): update @crabnebula/test-runner-backend to 0.3.1 0.3.0 returns a new-session response that WebdriverIO 10 rejects as not W3C, so every macOS e2e spec failed to create a session. --------- Co-authored-by: Lucas Nogueira <lucas@tauri.app>
This commit is contained in:
1 parent
f36ba7ba48
commit
11211c30e7
6 files changed
+230
-128
No files matched your search
@@ -0,0 +1,6 @@
|
||||
---
|
||||
window-state: patch
|
||||
window-state-js: patch
|
||||
---
|
||||
|
||||
Fixed windows using `titleBarStyle: Overlay` on macOS growing by the title bar height every time their size was restored. Also fixed a maximized window being restored off-screen when the position it had before being maximized is on a monitor that is no longer connected.
|
||||
@@ -86,7 +86,7 @@ skipped on desktop, and the rest run everywhere with the odd test gated.
|
||||
| `updater` | `check` against the fixture manifest (update / 204 / older release). Installing is never exercised. | Skipped — desktop-only plugin. |
|
||||
| `upload` | `download` and `upload` with progress, methods, headers and error paths. | Same (through `adb reverse` on Android). |
|
||||
| `websocket` | Text/binary echo, ping/pong, handshake headers, listener removal, server and client close, connection and argument errors, against the fixture server. | Same (through `adb reverse` on Android). |
|
||||
| `window-state` | `filename`, save/restore, and that a re-created window gets its saved size (WM dependent). | Skipped — desktop-only plugin. |
|
||||
| `window-state` | `filename`, save/restore, and that a re-created window keeps its saved size (also with an overlay title bar) and position over repeated restores (WM dependent). | Skipped — desktop-only plugin. |
|
||||
| `biometric` | The `passcodeNotSet` rejection when `allowDeviceCredential` is set and the device has no lock screen (checked over `adb`). On an emulator without a lock screen, a PIN and fingerprint are enrolled (then removed) to cover `biometricCipher`: the encrypt/decrypt round trip, tampered and malformed input, and canceling the prompt, with the sensor touched over `adb emu`. | Android only; skipped on desktop. |
|
||||
| `geolocation` | `getCurrentPosition`, `watchPosition` (several at once) and `clearWatch` against a location simulated from the host, the granted permission state, and the rejections with location services off (Android). |
|
||||
| mobile-only plugins | `barcode-scanner`, `haptics` and `nfc` need hardware or native UI the driver cannot operate, so only their global API surface is asserted. | Only there. |
|
||||
@@ -172,7 +172,7 @@ passed there: both things it turns on belong to desktop-only plugins.
|
||||
| `E2E_SKIP_BUILD` | Skip the `tauri build` step and reuse the existing binary. |
|
||||
| `E2E_APP_PATH` | Absolute path to a prebuilt app/binary to test (also implies skip-build). |
|
||||
| `E2E_SKIP` | Comma-separated plugin names to skip, e.g. `E2E_SKIP=clipboard-manager,global-shortcut`. |
|
||||
| `E2E_SKIP_WM` | Skip window-manager-dependent tests (the window-state size restore). |
|
||||
| `E2E_SKIP_WM` | Skip window-manager-dependent tests (the window-state size and position restores). |
|
||||
| `E2E_SPEC_RETRIES` | Retry count for flaky spec files (default `0`). |
|
||||
| `E2E_CN_WEBDRIVER` | Use the CrabNebula Webdriver on Linux/Windows too (instead of the native driver). |
|
||||
| `E2E_NATIVE_DRIVER` | Path passed to `tauri-driver --native-driver` (e.g. a specific chromedriver). |
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@crabnebula/tauri-driver": "^2.0.9",
|
||||
"@crabnebula/test-runner-backend": "^0.3.0",
|
||||
"@crabnebula/test-runner-backend": "^0.3.1",
|
||||
"@tauri-apps/api": "^2.12.0",
|
||||
"@tauri-apps/plugin-autostart": "workspace:*",
|
||||
"@tauri-apps/plugin-barcode-scanner": "workspace:*",
|
||||
|
||||
@@ -5,6 +5,146 @@
|
||||
import { expect } from '@wdio/globals'
|
||||
import { tauri, eventually, describePlugin, itWm } from '../helpers/index.js'
|
||||
|
||||
interface WindowOptions {
|
||||
width?: number
|
||||
height?: number
|
||||
x?: number
|
||||
y?: number
|
||||
titleBarStyle?: 'visible' | 'transparent' | 'overlay'
|
||||
}
|
||||
|
||||
interface Geometry {
|
||||
inner: { width: number; height: number }
|
||||
outer: { width: number; height: number }
|
||||
position: { x: number; y: number }
|
||||
}
|
||||
|
||||
const createWindow = (label: string, options: WindowOptions) =>
|
||||
tauri(
|
||||
(api, label, options) =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
const window = new api.webviewWindow.WebviewWindow(label, options)
|
||||
window.once('tauri://created', () => resolve()).catch(reject)
|
||||
window
|
||||
.once('tauri://error', (event) =>
|
||||
reject(
|
||||
new Error(`window creation failed: ${String(event.payload)}`)
|
||||
)
|
||||
)
|
||||
.catch(reject)
|
||||
setTimeout(() => reject(new Error('window creation timed out')), 8000)
|
||||
}),
|
||||
label,
|
||||
options
|
||||
)
|
||||
|
||||
const geometry = (label: string) =>
|
||||
tauri(async (api, label): Promise<Geometry> => {
|
||||
const window = await api.webviewWindow.WebviewWindow.getByLabel(label)
|
||||
if (!window) throw new Error(`window ${label} not found`)
|
||||
const inner = await window.innerSize()
|
||||
const outer = await window.outerSize()
|
||||
const position = await window.outerPosition()
|
||||
return {
|
||||
inner: { width: inner.width, height: inner.height },
|
||||
outer: { width: outer.width, height: outer.height },
|
||||
position: { x: position.x, y: position.y }
|
||||
}
|
||||
}, label)
|
||||
|
||||
const closeWindow = async (label: string) => {
|
||||
await tauri(async (api, label) => {
|
||||
const window = await api.webviewWindow.WebviewWindow.getByLabel(label)
|
||||
await window?.close()
|
||||
return null
|
||||
}, label)
|
||||
await eventually(async () => {
|
||||
const labels = await tauri(async (api) =>
|
||||
(await api.webviewWindow.getAllWebviewWindows()).map((w) => w.label)
|
||||
)
|
||||
if (labels.includes(label)) {
|
||||
throw new Error('window is still present after close')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const saveState = (flags: 'SIZE' | 'POSITION') =>
|
||||
tauri(
|
||||
(api, flags) =>
|
||||
api.windowState.saveWindowState(
|
||||
flags === 'SIZE'
|
||||
? api.windowState.StateFlags.SIZE
|
||||
: api.windowState.StateFlags.POSITION
|
||||
),
|
||||
flags
|
||||
)
|
||||
|
||||
const near = (actual: number, expected: number, tolerance: number) =>
|
||||
Math.abs(actual - expected) <= tolerance
|
||||
|
||||
/**
|
||||
* Creates the window `cycles` times. Each time it asks for a size and
|
||||
* position different from the saved state, and the window must come back with
|
||||
* the geometry of the first one, so any drift between saving and restoring
|
||||
* (a title bar counted twice, a scale applied twice) accumulates and shows.
|
||||
*/
|
||||
async function expectStableRoundTrip(
|
||||
label: string,
|
||||
options: WindowOptions,
|
||||
flags: 'SIZE' | 'POSITION',
|
||||
cycles = 3
|
||||
) {
|
||||
const scale = await tauri((api) =>
|
||||
api.window.getCurrentWindow().scaleFactor()
|
||||
)
|
||||
const tolerance = Math.ceil(scale) * 2
|
||||
|
||||
await createWindow(label, options)
|
||||
// let the window settle before reading what will be saved
|
||||
const saved = await eventually(async () => {
|
||||
const first = await geometry(label)
|
||||
await new Promise((resolve) => setTimeout(resolve, 300))
|
||||
const second = await geometry(label)
|
||||
if (JSON.stringify(first) !== JSON.stringify(second)) {
|
||||
throw new Error('window geometry is still changing')
|
||||
}
|
||||
return second
|
||||
})
|
||||
await saveState(flags)
|
||||
await closeWindow(label)
|
||||
|
||||
for (let cycle = 1; cycle <= cycles; cycle++) {
|
||||
await createWindow(label, {
|
||||
...options,
|
||||
width: (options.width ?? 500) + 200,
|
||||
height: (options.height ?? 400) + 150,
|
||||
x: (options.x ?? 100) + 120,
|
||||
y: (options.y ?? 100) + 90
|
||||
})
|
||||
try {
|
||||
await eventually(async () => {
|
||||
const actual = await geometry(label)
|
||||
const ok =
|
||||
flags === 'SIZE'
|
||||
? near(actual.inner.width, saved.inner.width, tolerance)
|
||||
&& near(actual.inner.height, saved.inner.height, tolerance)
|
||||
&& near(actual.outer.width, saved.outer.width, tolerance)
|
||||
&& near(actual.outer.height, saved.outer.height, tolerance)
|
||||
: near(actual.position.x, saved.position.x, tolerance)
|
||||
&& near(actual.position.y, saved.position.y, tolerance)
|
||||
if (!ok) {
|
||||
throw new Error(
|
||||
`cycle ${cycle}: ${JSON.stringify(actual)} does not match the saved ${JSON.stringify(saved)}`
|
||||
)
|
||||
}
|
||||
})
|
||||
await saveState(flags)
|
||||
} finally {
|
||||
await closeWindow(label)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The plugin is desktop-only: a mobile window is the whole screen and has no
|
||||
// state to persist, so the whole suite is skipped there.
|
||||
describePlugin('window-state', { desktopOnly: true }, () => {
|
||||
@@ -35,96 +175,31 @@ describePlugin('window-state', { desktopOnly: true }, () => {
|
||||
)
|
||||
})
|
||||
|
||||
itWm(
|
||||
'a new window is restored to the size it was last saved with',
|
||||
async () => {
|
||||
// The plugin applies the cached state of a label whenever a window with
|
||||
// that label is created, which is what persists sizes across sessions.
|
||||
const label = 'e2e-window-state'
|
||||
const scale = await tauri((api) =>
|
||||
api.window.getCurrentWindow().scaleFactor()
|
||||
)
|
||||
// The plugin applies the cached state of a label whenever a window with
|
||||
// that label is created, which is what persists state across sessions.
|
||||
itWm('a new window is restored to the size it was last saved with', () =>
|
||||
expectStableRoundTrip(
|
||||
'e2e-window-state-size',
|
||||
{ width: 500, height: 400 },
|
||||
'SIZE'
|
||||
)
|
||||
)
|
||||
|
||||
const create = (width: number, height: number) =>
|
||||
tauri(
|
||||
(api, label, width, height) =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
const window = new api.webviewWindow.WebviewWindow(label, {
|
||||
width,
|
||||
height
|
||||
})
|
||||
window.once('tauri://created', () => resolve()).catch(reject)
|
||||
window
|
||||
.once('tauri://error', (event) =>
|
||||
reject(
|
||||
new Error(
|
||||
`window creation failed: ${String(event.payload)}`
|
||||
)
|
||||
)
|
||||
)
|
||||
.catch(reject)
|
||||
setTimeout(
|
||||
() => reject(new Error('window creation timed out')),
|
||||
8000
|
||||
)
|
||||
}),
|
||||
label,
|
||||
width,
|
||||
height
|
||||
)
|
||||
const innerSize = () =>
|
||||
tauri(async (api, label) => {
|
||||
const window = await api.webviewWindow.WebviewWindow.getByLabel(label)
|
||||
if (!window) throw new Error(`window ${label} not found`)
|
||||
const size = await window.innerSize()
|
||||
return { width: size.width, height: size.height }
|
||||
}, label)
|
||||
const close = async () => {
|
||||
await tauri(async (api, label) => {
|
||||
const window = await api.webviewWindow.WebviewWindow.getByLabel(label)
|
||||
await window?.close()
|
||||
return null
|
||||
}, label)
|
||||
await eventually(async () => {
|
||||
const labels = await tauri(async (api) =>
|
||||
(await api.webviewWindow.getAllWebviewWindows()).map((w) => w.label)
|
||||
)
|
||||
if (labels.includes(label)) {
|
||||
throw new Error('window is still present after close')
|
||||
}
|
||||
})
|
||||
}
|
||||
const expectSize = (width: number, height: number) =>
|
||||
eventually(async () => {
|
||||
const size = await innerSize()
|
||||
const tolerance = Math.ceil(scale) * 8
|
||||
if (
|
||||
Math.abs(size.width - width) > tolerance
|
||||
|| Math.abs(size.height - height) > tolerance
|
||||
) {
|
||||
throw new Error(
|
||||
`size ${size.width}x${size.height} not near ${width}x${height}`
|
||||
)
|
||||
}
|
||||
})
|
||||
// An overlay title bar draws over the content, so the webview is as tall as
|
||||
// the window. Restoring must not add or remove the title bar height.
|
||||
itWm('an overlay title bar window keeps its size across restores', () =>
|
||||
expectStableRoundTrip(
|
||||
'e2e-window-state-overlay',
|
||||
{ width: 500, height: 400, titleBarStyle: 'overlay' },
|
||||
'SIZE'
|
||||
)
|
||||
)
|
||||
|
||||
// create the window at one size and save that state
|
||||
await create(500, 400)
|
||||
await expectSize(500 * scale, 400 * scale)
|
||||
const saved = await innerSize()
|
||||
await tauri((api) =>
|
||||
api.windowState.saveWindowState(api.windowState.StateFlags.SIZE)
|
||||
)
|
||||
await close()
|
||||
|
||||
// a new window with the same label asks for another size, and gets the
|
||||
// saved one back
|
||||
await create(700, 600)
|
||||
try {
|
||||
await expectSize(saved.width, saved.height)
|
||||
} finally {
|
||||
await close()
|
||||
}
|
||||
}
|
||||
itWm('a new window is restored to the position it was last saved at', () =>
|
||||
expectStableRoundTrip(
|
||||
'e2e-window-state-position',
|
||||
{ width: 500, height: 400, x: 150, y: 120 },
|
||||
'POSITION'
|
||||
)
|
||||
)
|
||||
})
|
||||
@@ -204,24 +204,20 @@ impl<R: Runtime> WindowExt for Window<R> {
|
||||
}
|
||||
|
||||
if flags.contains(StateFlags::POSITION) {
|
||||
let position = (state.x, state.y).into();
|
||||
// a maximized window's x and y are the monitor's corner, so it is
|
||||
// placed where it was before it was maximized
|
||||
let position = if state.maximized {
|
||||
PhysicalPosition::new(state.prev_x, state.prev_y)
|
||||
} else {
|
||||
PhysicalPosition::new(state.x, state.y)
|
||||
};
|
||||
let size = (state.width, state.height).into();
|
||||
// restore position to saved value if saved monitor exists
|
||||
// otherwise, let the OS decide where to place the window
|
||||
for m in self.available_monitors()? {
|
||||
if m.intersects(position, size) {
|
||||
self.set_position(PhysicalPosition {
|
||||
x: if state.maximized {
|
||||
state.prev_x
|
||||
} else {
|
||||
state.x
|
||||
},
|
||||
y: if state.maximized {
|
||||
state.prev_y
|
||||
} else {
|
||||
state.y
|
||||
},
|
||||
})?;
|
||||
self.set_position(position)?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -246,7 +242,7 @@ impl<R: Runtime> WindowExt for Window<R> {
|
||||
let mut metadata = WindowState::default();
|
||||
|
||||
if flags.contains(StateFlags::SIZE) {
|
||||
let size = self.inner_size()?;
|
||||
let size = restorable_size(self)?;
|
||||
metadata.width = size.width;
|
||||
metadata.height = size.height;
|
||||
}
|
||||
@@ -285,6 +281,27 @@ impl<R: Runtime> WindowExt for Window<R> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The size to save for `window`, so that restoring it with `set_size` recreates the window.
|
||||
///
|
||||
/// `set_size` sets the inner size, which on macOS is the window's content rect: the frame minus
|
||||
/// the title bar. `inner_size` reports the webview's bounds instead, which include the title bar
|
||||
/// when it is drawn over the content (`TitleBarStyle::Overlay`), so saving it would grow the
|
||||
/// window by the title bar height on every restore. `inner_position` is the content rect's
|
||||
/// origin, so its offset from `outer_position` is the title bar height.
|
||||
fn restorable_size<R: Runtime>(window: &Window<R>) -> tauri::Result<PhysicalSize<u32>> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let outer = window.outer_size()?;
|
||||
let title_bar = window.inner_position()?.y - window.outer_position()?.y;
|
||||
Ok(PhysicalSize::new(
|
||||
outer.width,
|
||||
outer.height.saturating_sub(title_bar.max(0) as u32),
|
||||
))
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
window.inner_size()
|
||||
}
|
||||
|
||||
trait WindowExtInternal {
|
||||
fn update_state(&self, state: &mut WindowState, flags: StateFlags) -> tauri::Result<()>;
|
||||
}
|
||||
@@ -320,7 +337,7 @@ impl<R: Runtime> WindowExtInternal for Window<R> {
|
||||
}
|
||||
|
||||
if flags.contains(StateFlags::SIZE) && !is_maximized && !is_minimized {
|
||||
let size = self.inner_size()?;
|
||||
let size = restorable_size(self)?;
|
||||
// It doesn't make sense to save a window with 0 height or width
|
||||
if size.width > 0 && size.height > 0 {
|
||||
state.width = size.width;
|
||||
@@ -517,6 +534,10 @@ impl Builder {
|
||||
if !window_clone.is_minimized().unwrap_or_default() && !is_maximized {
|
||||
let mut c = cache.lock().unwrap();
|
||||
if let Some(state) = c.get_mut(&label) {
|
||||
// the event carries the inner size, which is not what
|
||||
// macOS restores (see `restorable_size`)
|
||||
#[cfg(target_os = "macos")]
|
||||
let size = &restorable_size(&window_clone).unwrap_or(*size);
|
||||
state.width = size.width;
|
||||
state.height = size.height;
|
||||
}
|
||||
|
||||
Generated
+21
-21
@@ -270,8 +270,8 @@ importers:
|
||||
specifier: ^2.0.9
|
||||
version: 2.0.9
|
||||
'@crabnebula/test-runner-backend':
|
||||
specifier: ^0.3.0
|
||||
version: 0.3.0
|
||||
specifier: ^0.3.1
|
||||
version: 0.3.1
|
||||
'@tauri-apps/api':
|
||||
specifier: ^2.12.0
|
||||
version: 2.12.1
|
||||
@@ -712,33 +712,33 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
hasBin: true
|
||||
|
||||
'@crabnebula/test-runner-backend-darwin-arm64@0.3.0':
|
||||
resolution: {integrity: sha512-XnZdSeew2ew/Eix+z1BwKt5PcrJSjL+VQ7dlrV1fvRj4WUTQtluzE1VhiVV0j2EIFJsRH2IExNj5OdCWxKSPiQ==}
|
||||
'@crabnebula/test-runner-backend-darwin-arm64@0.3.1':
|
||||
resolution: {integrity: sha512-5oZYngc7nGPiiWBnM48+nLVGt4/6oTNPnBt8uSz3AZDVo6bpQ/PdtBowOBaPhW8FG8wJCkCywuPKZkxNiQbQcw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@crabnebula/test-runner-backend-darwin-x64@0.3.0':
|
||||
resolution: {integrity: sha512-dpogk8TxKd2CZb8BZ2Ni1mpbogD8KSoB8J92aw+9R24S8xBeTu8I+t4w9PVNIBvFaNAhEQelYhFffxH0FDs5+Q==}
|
||||
'@crabnebula/test-runner-backend-darwin-x64@0.3.1':
|
||||
resolution: {integrity: sha512-dAm10Qii1AgvhNzE4OaEHJ2KiIhbu1xqwtzkWEdvVNbBeoHoAk126o6WvluPkjMSXmys2Hzza+PsaXSuwzsUMQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@crabnebula/test-runner-backend-linux-x64-gnu@0.3.0':
|
||||
resolution: {integrity: sha512-ZzDVKdY1ydv+lqfRYs6PAdmIc8AlfOciH3ntuUxyRJ8aSq0v8y+pow6b3/CRqmumkj9riVWsUriJr7Xw2YZFTg==}
|
||||
'@crabnebula/test-runner-backend-linux-x64-gnu@0.3.1':
|
||||
resolution: {integrity: sha512-zv5aG7JZVoZ024CzgmxUru6W7MxCfrE79WWDnPoo2/Xl8CO9fFaUSlUkGluUvNkd+0aufcMT0TGMCYhN/c1pEA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@crabnebula/test-runner-backend-win32-x64-msvc@0.3.0':
|
||||
resolution: {integrity: sha512-xzE5bZx6QpqCkGFTpOYGxMfYidfsvYQgp3yQ1hXwuxeC5vsabOngRtE6X2DxQU6U8JzzNaooN0afG/khzoaihQ==}
|
||||
'@crabnebula/test-runner-backend-win32-x64-msvc@0.3.1':
|
||||
resolution: {integrity: sha512-Zlm3a7I+p+4DTVkspbUS3xsXePJ++7IMP5lI5ONGuYtPg7/qOYtUksjfT/LpCd4pqoCAqGr7Qr5icNWDaZMIag==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@crabnebula/test-runner-backend@0.3.0':
|
||||
resolution: {integrity: sha512-72aldFCPV0Sflv8KGLeaNoUY4Vz8eRl7AvUsJXFwqJ7KKNDAfPKwovXjmZoqIWn6Icv01sEsd1b2w0O66Y3WEQ==}
|
||||
'@crabnebula/test-runner-backend@0.3.1':
|
||||
resolution: {integrity: sha512-gDYc+dj/K8jyNvJBH7WUjXrZi7y16qXEJXoW82Im71BbMpwqAknRm0HlmFmvuBK0Y+VCkSaD6VuTbPbfHsIvww==}
|
||||
engines: {node: '>= 10'}
|
||||
hasBin: true
|
||||
|
||||
@@ -4821,24 +4821,24 @@ snapshots:
|
||||
'@crabnebula/tauri-driver-linux-x64-gnu': 2.0.9
|
||||
'@crabnebula/tauri-driver-win32-x64-msvc': 2.0.9
|
||||
|
||||
'@crabnebula/test-runner-backend-darwin-arm64@0.3.0':
|
||||
'@crabnebula/test-runner-backend-darwin-arm64@0.3.1':
|
||||
optional: true
|
||||
|
||||
'@crabnebula/test-runner-backend-darwin-x64@0.3.0':
|
||||
'@crabnebula/test-runner-backend-darwin-x64@0.3.1':
|
||||
optional: true
|
||||
|
||||
'@crabnebula/test-runner-backend-linux-x64-gnu@0.3.0':
|
||||
'@crabnebula/test-runner-backend-linux-x64-gnu@0.3.1':
|
||||
optional: true
|
||||
|
||||
'@crabnebula/test-runner-backend-win32-x64-msvc@0.3.0':
|
||||
'@crabnebula/test-runner-backend-win32-x64-msvc@0.3.1':
|
||||
optional: true
|
||||
|
||||
'@crabnebula/test-runner-backend@0.3.0':
|
||||
'@crabnebula/test-runner-backend@0.3.1':
|
||||
optionalDependencies:
|
||||
'@crabnebula/test-runner-backend-darwin-arm64': 0.3.0
|
||||
'@crabnebula/test-runner-backend-darwin-x64': 0.3.0
|
||||
'@crabnebula/test-runner-backend-linux-x64-gnu': 0.3.0
|
||||
'@crabnebula/test-runner-backend-win32-x64-msvc': 0.3.0
|
||||
'@crabnebula/test-runner-backend-darwin-arm64': 0.3.1
|
||||
'@crabnebula/test-runner-backend-darwin-x64': 0.3.1
|
||||
'@crabnebula/test-runner-backend-linux-x64-gnu': 0.3.1
|
||||
'@crabnebula/test-runner-backend-win32-x64-msvc': 0.3.1
|
||||
|
||||
'@dabh/diagnostics@2.0.9':
|
||||
dependencies:
|
||||
|
||||
Reference in new issue
Block a user