mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-09-24 21:40:48 +02:00
* feat(tests): add api e2e tests * lockfile * mobile * try linux fix [skip ci] * ios fix * fix test on windows * improve cache * fix pnpm audit [skip ci]
239 lines
8.2 KiB
TypeScript
239 lines
8.2 KiB
TypeScript
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
import { expect } from '@wdio/globals'
|
|
import {
|
|
tauri,
|
|
tauriError,
|
|
describePlugin,
|
|
platform
|
|
} from '../helpers/index.js'
|
|
|
|
// The example's shell scope allows `sh -c <script>` and `cmd /C <script>`.
|
|
const shell =
|
|
platform === 'win32'
|
|
? { program: 'cmd', flag: '/C' }
|
|
: { program: 'sh', flag: '-c' }
|
|
|
|
// Running a child process works on desktop and on Android (`/system/bin/sh`),
|
|
// but iOS does not let an app spawn one at all. The scope specs below stay on
|
|
// every platform: `prepare_cmd` rejects before anything is executed.
|
|
const itSpawns = platform === 'ios' ? it.skip : it
|
|
|
|
/**
|
|
* Puts a directory in the form the two sides of the working directory
|
|
* assertion can be compared in: the shell may print it with a different path
|
|
* style, and on Android `/data/user/<n>/<pkg>` (which `appDataDir` reports) is
|
|
* a symlink to `/data/data/<pkg>` (which `pwd` resolves it to).
|
|
*/
|
|
function normalizeDir(dir: string): string {
|
|
return dir
|
|
.replace(/[\\/]+$/, '')
|
|
.replace(/\\/g, '/')
|
|
.replace(/^\/data\/user\/\d+\//, '/data/data/')
|
|
.toLowerCase()
|
|
}
|
|
|
|
describePlugin('shell', () => {
|
|
itSpawns('execute collects stdout, stderr and the exit code', async () => {
|
|
const output = await tauri(
|
|
async (api, program, flag, script) => {
|
|
const result = await api.shell.Command.create(program, [
|
|
flag,
|
|
script
|
|
]).execute()
|
|
return {
|
|
code: result.code,
|
|
signal: result.signal,
|
|
stdout: result.stdout.trim(),
|
|
stderr: result.stderr.trim()
|
|
}
|
|
},
|
|
shell.program,
|
|
shell.flag,
|
|
platform === 'win32'
|
|
? 'echo hello from e2e && echo warning 1>&2 && exit 3'
|
|
: 'echo "hello from e2e"; echo "warning" >&2; exit 3'
|
|
)
|
|
expect(output.code).toBe(3)
|
|
expect(output.signal).toBeNull()
|
|
expect(output.stdout).toBe('hello from e2e')
|
|
expect(output.stderr).toBe('warning')
|
|
})
|
|
|
|
itSpawns(
|
|
'execute passes environment variables and the working directory',
|
|
async () => {
|
|
const cwd = await tauri((api) => api.path.appDataDir())
|
|
const output = await tauri(
|
|
async (api, program, flag, script, cwd) => {
|
|
const result = await api.shell.Command.create(
|
|
program,
|
|
[flag, script],
|
|
{
|
|
cwd,
|
|
env: { E2E_VALUE: 'from-e2e' }
|
|
}
|
|
).execute()
|
|
return { code: result.code, stdout: result.stdout.trim() }
|
|
},
|
|
shell.program,
|
|
shell.flag,
|
|
platform === 'win32'
|
|
? 'echo %E2E_VALUE% && cd'
|
|
: 'echo "$E2E_VALUE"; pwd',
|
|
cwd
|
|
)
|
|
expect(output.code).toBe(0)
|
|
// `cmd` echoes everything between `echo ` and `&&`, the space before the
|
|
// separator included, so every line is trimmed and not just the ends of
|
|
// the output as a whole.
|
|
const [value, reportedCwd] = output.stdout
|
|
.split(/\r?\n/)
|
|
.map((line) => line.trim())
|
|
expect(value).toBe('from-e2e')
|
|
expect(normalizeDir(reportedCwd ?? '')).toBe(normalizeDir(cwd))
|
|
}
|
|
)
|
|
|
|
itSpawns(
|
|
'spawn streams stdout and stderr lines and reports close',
|
|
async () => {
|
|
const events = await tauri(
|
|
(api, program, flag, script) =>
|
|
new Promise<{
|
|
stdout: string[]
|
|
stderr: string[]
|
|
close: { code: number | null; signal: number | null }
|
|
pid: number
|
|
}>((resolve, reject) => {
|
|
const stdout: string[] = []
|
|
const stderr: string[] = []
|
|
let pid = 0
|
|
const command = api.shell.Command.create(program, [flag, script])
|
|
command.stdout.on('data', (line) => stdout.push(line.trim()))
|
|
command.stderr.on('data', (line) => stderr.push(line.trim()))
|
|
command.on('error', (error) => reject(new Error(error)))
|
|
command.on('close', (payload) =>
|
|
resolve({
|
|
stdout,
|
|
stderr,
|
|
close: { code: payload.code, signal: payload.signal },
|
|
pid
|
|
})
|
|
)
|
|
command
|
|
.spawn()
|
|
.then((child) => {
|
|
pid = child.pid
|
|
})
|
|
.catch(reject)
|
|
setTimeout(() => reject(new Error('command never closed')), 15000)
|
|
}),
|
|
shell.program,
|
|
shell.flag,
|
|
platform === 'win32'
|
|
? 'echo one && echo two && echo err 1>&2'
|
|
: 'echo one; echo two; echo err >&2'
|
|
)
|
|
expect(events.pid).toBeGreaterThan(0)
|
|
expect(events.stdout).toEqual(['one', 'two'])
|
|
expect(events.stderr).toEqual(['err'])
|
|
expect(events.close.code).toBe(0)
|
|
}
|
|
)
|
|
|
|
itSpawns('write sends to stdin', async () => {
|
|
const output = await tauri(
|
|
(api, program, flag, script) =>
|
|
new Promise<string>((resolve, reject) => {
|
|
let out = ''
|
|
const command = api.shell.Command.create(program, [flag, script])
|
|
command.stdout.on('data', (line) => {
|
|
out += line
|
|
})
|
|
command.on('error', (error) => reject(new Error(error)))
|
|
command.on('close', () => resolve(out.trim()))
|
|
command
|
|
.spawn()
|
|
.then((child) => child.write('ping from e2e\n'))
|
|
.catch(reject)
|
|
setTimeout(() => reject(new Error('command never closed')), 15000)
|
|
}),
|
|
shell.program,
|
|
shell.flag,
|
|
// `call` makes cmd expand `%line%` after `set /p` ran, rather than
|
|
// when the line is parsed
|
|
platform === 'win32'
|
|
? 'set /p line= & call echo got: %line%'
|
|
: 'read line; echo "got: $line"'
|
|
)
|
|
expect(output).toBe('got: ping from e2e')
|
|
})
|
|
|
|
itSpawns('kill terminates a running child', async () => {
|
|
const result = await tauri(
|
|
(api, program, flag, script) =>
|
|
new Promise<{ code: number | null; signal: number | null }>(
|
|
(resolve, reject) => {
|
|
const command = api.shell.Command.create(program, [flag, script])
|
|
command.on('error', (error) => reject(new Error(error)))
|
|
command.on('close', (payload) =>
|
|
resolve({ code: payload.code, signal: payload.signal })
|
|
)
|
|
command
|
|
.spawn()
|
|
.then((child) =>
|
|
// give the shell a moment to start before killing it
|
|
new Promise((r) => setTimeout(r, 500)).then(() => child.kill())
|
|
)
|
|
.catch(reject)
|
|
setTimeout(() => reject(new Error('child was not killed')), 15000)
|
|
}
|
|
),
|
|
shell.program,
|
|
shell.flag,
|
|
// The script has to keep the shell itself busy rather than start another
|
|
// process: `kill` only signals the direct child, and a surviving
|
|
// grandchild holds the stdout/stderr pipes open, which withholds the
|
|
// `close` event until it exits on its own. Both shells block on their
|
|
// built-in stdin read, and the test never writes to stdin.
|
|
platform === 'win32' ? 'set /p killme=' : 'read killme'
|
|
)
|
|
if (platform === 'win32') {
|
|
// TerminateProcess sets an exit code of 1
|
|
expect(result.code).not.toBe(0)
|
|
} else {
|
|
// killed by SIGKILL, so no exit code
|
|
expect(result.code).toBeNull()
|
|
expect(result.signal).toBe(9)
|
|
}
|
|
})
|
|
|
|
it('rejects programs that are not in the scope', async () => {
|
|
const message = await tauriError((api) =>
|
|
api.shell.Command.create('e2e-not-allowed').execute()
|
|
)
|
|
expect(message).toMatch(/program not allowed on the configured shell scope/)
|
|
})
|
|
|
|
it('rejects arguments that do not match the scope', async () => {
|
|
// the scope only allows `-c`/`/C` followed by a non-empty script
|
|
const message = await tauriError(
|
|
(api, program) =>
|
|
api.shell.Command.create(program, ['--version']).execute(),
|
|
shell.program
|
|
)
|
|
expect(message).toMatch(/not allowed|validator|scope/i)
|
|
})
|
|
|
|
it('open rejects URLs outside the default scope', async () => {
|
|
// `shell:default` only allows http(s), mailto and tel URLs
|
|
const message = await tauriError((api) =>
|
|
api.shell.open('ftp://example.com')
|
|
)
|
|
expect(message.length).toBeGreaterThan(0)
|
|
})
|
|
})
|