diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 118e4ae..6b61d51 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -21,10 +21,20 @@ jobs:
with:
node-version: "20"
+ - name: Install pinned Google Chrome
+ run: |
+ set -euo pipefail
+ sudo apt-get update
+ CHROME_VERSION="147.0.7727.137-1"
+ CHROME_DEB="google-chrome-stable_${CHROME_VERSION}_amd64.deb"
+ curl -fsSLo "$CHROME_DEB" "https://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/${CHROME_DEB}"
+ sudo apt install -y --allow-downgrades "./${CHROME_DEB}"
+ google-chrome --version
+
- name: Install dependencies
run: |
set -euo pipefail
- npm install
+ npm ci
- name: Run checks
run: |
@@ -33,4 +43,6 @@ jobs:
node tests/test_auth_code_extraction.js
node tests/test_auth_retry.js
node tests/test_proxy_config.js
+ node tests/test_totp_dependency.js
+ node tests/test_playwright_dependency.js
bash tests/test_workflow_config_sources.sh
diff --git a/tests/test_playwright_dependency.js b/tests/test_playwright_dependency.js
new file mode 100644
index 0000000..42df30f
--- /dev/null
+++ b/tests/test_playwright_dependency.js
@@ -0,0 +1,40 @@
+const assert = require('assert');
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+const { chromium } = require('playwright-extra');
+const stealth = require('puppeteer-extra-plugin-stealth')();
+
+chromium.use(stealth);
+
+(async () => {
+ const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'schwab-playwright-smoke-'));
+ const context = await chromium.launchPersistentContext(userDataDir, {
+ channel: 'chrome',
+ headless: true,
+ });
+ try {
+ const page = await context.newPage();
+ await page.setContent(`
+
+
+
+ `);
+
+ await page.getByRole('button', { name: 'Continue' }).click();
+ await assert.doesNotReject(() => page.getByText('continued').waitFor());
+ assert.strictEqual(await page.locator('#status').textContent(), 'continued');
+ } finally {
+ await context.close();
+ fs.rmSync(userDataDir, { recursive: true, force: true });
+ }
+
+ console.log('playwright-extra Chrome channel smoke passed');
+})().catch(error => {
+ console.error(error);
+ process.exitCode = 1;
+});
diff --git a/tests/test_totp_dependency.js b/tests/test_totp_dependency.js
new file mode 100644
index 0000000..306bf35
--- /dev/null
+++ b/tests/test_totp_dependency.js
@@ -0,0 +1,35 @@
+const assert = require('assert');
+const { Secret, TOTP } = require('otpauth');
+
+// RFC 6238 SHA-1 vector at Unix time 59 seconds.
+const base32Secret = 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ';
+const expectedCode = '94287082';
+
+assert.strictEqual(
+ new TOTP({ secret: base32Secret, algorithm: 'SHA1', digits: 8, period: 30 })
+ .generate({ timestamp: 59000 }),
+ expectedCode,
+);
+
+assert.strictEqual(
+ new TOTP({
+ secret: ` ${base32Secret.toLowerCase().slice(0, 10)}\n${base32Secret.toLowerCase().slice(10)} `
+ .replace(/\s/g, ''),
+ algorithm: 'SHA1',
+ digits: 8,
+ period: 30,
+ }).generate({ timestamp: 59000 }),
+ expectedCode,
+);
+
+assert.strictEqual(
+ new TOTP({
+ secret: Secret.fromHex('3132333435363738393031323334353637383930'),
+ algorithm: 'SHA1',
+ digits: 8,
+ period: 30,
+ }).generate({ timestamp: 59000 }),
+ expectedCode,
+);
+
+console.log('otpauth RFC 6238 vectors passed');