diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b227db9..1f2137a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,3 +21,9 @@ jobs: node-version: 16 - run: yarn install --frozen-lockfile - run: yarn ci + - uses: actions/upload-artifact@v2 + if: always() + with: + name: artifacts + path: | + **/recordings/**/* diff --git a/README.md b/README.md index bc1f25e..49ad510 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,10 @@ If you are using GitHub Actions, check out the dedicated [`guidepup/setup-action uses: guidepup/setup-action@0.4.0 ``` +## Debugging + +If you are encountering errors in CI you can pass a `--record` flag to the command which will output a screen-recording of the setup to a `./recordings/` directory. + ## See Also 🐶 Check out some of the other Guidepup modules: diff --git a/package.json b/package.json index 4e7657c..cd9c832 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@guidepup/setup", - "version": "0.4.1", + "version": "0.5.0", "description": "Setup your environment for screen-reader automation.", "main": "lib/index.js", "typings": "lib/index.d.ts", @@ -25,7 +25,7 @@ ], "scripts": { "build": "yarn clean && yarn compile", - "ci": "yarn clean && yarn lint && yarn build && yarn start --ci", + "ci": "yarn clean && yarn lint && yarn build && yarn start --ci --record", "clean": "rimraf lib", "compile": "tsc", "dev": "ts-node ./src/index.ts", diff --git a/src/macOS/enableDoNotDisturb.ts b/src/macOS/enableDoNotDisturb.ts index 0e5839e..617c725 100644 --- a/src/macOS/enableDoNotDisturb.ts +++ b/src/macOS/enableDoNotDisturb.ts @@ -12,36 +12,29 @@ killall usernoted && killall ControlCenter // source https://www.reddit.com/r/applescript/comments/r9nnil/enable_do_not_disturb_on_monterey/ const enableFocusModeAppleScript = ` -tell application "System Preferences" - activate -end tell +set timeoutSeconds to 5.0 -tell application "System Events" - tell process "System Preferences" - repeat 25 times - if (exists window "System Preferences") then - click button "Notifications\n& Focus" of scroll area 1 of window "System Preferences" - repeat 25 times - if (exists window "Notifications & Focus") then - click radio button "Focus" of tab group 1 of window "Notifications & Focus" - set focusSwitch to checkbox 1 of group 1 of tab group 1 of window "Notifications & Focus" - tell focusSwitch - if not (its value as boolean) then click focusSwitch - end tell - tell application "System Preferences" - quit - end tell - return - end if - delay 0.2 - end repeat - error number 1 - end if - delay 0.2 - end repeat - error number 1 - end tell -end tell +set openSystemPreferences to "tell application \\"System Preferences\\" to activate" + +set clickNotificationAndFocusButton to "click UI Element \\"Notifications +& Focus\\" of scroll area 1 of window \\"System Preferences\\" of application process \\"System Preferences\\"" + +set clickFocusTab to "click radio button \\"Focus\\" of tab group 1 of window \\"Notifications & Focus\\" of application process \\"System Preferences\\"" + +set enableDoNotDisturb to " +set doNotDisturbToggle to checkbox 1 of group 1 of tab group 1 of window \\"Notifications & Focus\\" of application process \\"System Preferences\\" + +tell doNotDisturbToggle + if not (its value as boolean) then click doNotDisturbToggle +end tell" + +set closeSystemPreferences to "tell application \\"System Preferences\\" to quit" + +my withTimeout(openSystemPreferences, timeoutSeconds) +my withTimeout(clickNotificationAndFocusButton, timeoutSeconds) +my withTimeout(clickFocusTab, timeoutSeconds) +my withTimeout(enableDoNotDisturb, timeoutSeconds) +my withTimeout(closeSystemPreferences, timeoutSeconds) `; export async function enableDoNotDisturb() { @@ -57,7 +50,7 @@ export async function enableDoNotDisturb() { // From MacOS 12 Monterey (Darwin 21) there is no known way to enable DND via system defaults try { await runAppleScript(enableFocusModeAppleScript); - } catch (_) { + } catch (e) { throw new Error(ERR_MACOS_FAILED_TO_ENABLE_DO_NOT_DISTURB); } } diff --git a/src/macOS/record.ts b/src/macOS/record.ts new file mode 100644 index 0000000..9d423bc --- /dev/null +++ b/src/macOS/record.ts @@ -0,0 +1,32 @@ +import { execSync, spawn } from "child_process"; +import { dirname } from "path"; +import { unlinkSync } from "fs"; + +/** + * Start a screen recording. + * + * @param {string} filepath The file path to save the screen recording to. + * @returns {Function} A function to stop the screen recording. + */ +export function record(filepath: string): () => void { + execSync(`mkdir -p ${dirname(filepath)}`); + + try { + unlinkSync(filepath); + } catch (_) { + // file doesn't exist. + } + + const screencapture = spawn("/usr/sbin/screencapture", [ + "-v", + "-C", + "-k", + "-T0", + "-g", + filepath, + ]); + + return () => { + screencapture.stdin.write("q"); + }; +} diff --git a/src/macOS/runAppleScript.ts b/src/macOS/runAppleScript.ts index 719c851..3521124 100644 --- a/src/macOS/runAppleScript.ts +++ b/src/macOS/runAppleScript.ts @@ -7,7 +7,29 @@ export async function runAppleScript( script: string, { timeout = DEFAULT_TIMEOUT } = { timeout: DEFAULT_TIMEOUT } ): Promise { - const scriptWithTimeout = `with timeout of ${timeout} seconds\n${script}\nend timeout`; + const scriptWithTimeout = ` +with timeout of ${timeout} seconds + ${script} +end timeout + +on withTimeout(uiScript, timeoutSeconds) + set endDate to (current date) + timeoutSeconds + repeat + try + run script "tell application \\"System Events\\" +" & uiScript & " +end tell" + exit repeat + on error errorMessage + if ((current date) > endDate) then + error errorMessage & "\n\nFor script: " & uiScript + end if + end try + + delay 0.2 + end repeat +end doWithTimeout +`; return (await new Promise((resolve, reject) => { const child = execFile( diff --git a/src/macOS/setup.ts b/src/macOS/setup.ts index 52c83d2..9febc57 100644 --- a/src/macOS/setup.ts +++ b/src/macOS/setup.ts @@ -11,46 +11,56 @@ import { setVoiceOverEnabledViaUi } from "./setVoiceOverEnabledViaUi"; import { logInfo } from "../logging"; import { ERR_MACOS_REQUIRES_MANUAL_USER_INTERACTION } from "../errors"; import { enableDoNotDisturb } from "./enableDoNotDisturb"; +import { record } from "./record"; const isCi = process.argv.includes("--ci"); +const isRecorded = process.argv.includes("--record"); export async function setup(): Promise { - checkVersion(); - enableAppleScriptControlSystemDefaults(); - disableSplashScreenSystemDefaults(); - disableDictationInputAutoEnable(); + const stopRecording = isRecorded + ? record(`./recordings/macos-setup-${+new Date()}.mov`) + : () => null; try { - updateTccDb(); - } catch (e) { - if (isCi) { - throw e; + checkVersion(); + enableAppleScriptControlSystemDefaults(); + disableSplashScreenSystemDefaults(); + disableDictationInputAutoEnable(); + + try { + updateTccDb(); + } catch (e) { + if (isCi) { + throw e; + } } - } - if (isCi) { - await enableDoNotDisturb(); - } + if (isCi) { + await enableDoNotDisturb(); + } - if (!isSipEnabled()) { - writeDatabaseFile(); + if (!isSipEnabled()) { + writeDatabaseFile(); - return; - } + return; + } - if (await isAppleScriptControlEnabled()) { - return; - } + if (await isAppleScriptControlEnabled()) { + return; + } - if (isCi) { - throw new Error(ERR_MACOS_REQUIRES_MANUAL_USER_INTERACTION); - } + if (isCi) { + throw new Error(ERR_MACOS_REQUIRES_MANUAL_USER_INTERACTION); + } - const credentials = await askUserToControlUi(); + const credentials = await askUserToControlUi(); - logInfo(""); - logInfo("Starting UI control..."); - logInfo("Please refrain from interaction until the script has completed"); + logInfo(""); + logInfo("Starting UI control..."); + logInfo("Please refrain from interaction until the script has completed"); - await setVoiceOverEnabledViaUi(credentials); + await setVoiceOverEnabledViaUi(credentials); + } finally { + stopRecording(); + } }