-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' of https://github.com/clr-li/AttendanceScanner
- Loading branch information
Showing
8 changed files
with
181 additions
and
36 deletions.
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,5 +5,5 @@ node_modules | |
.DS_Store | ||
.env | ||
.firebase | ||
test.log | ||
test.*.log | ||
tap.info |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -74,7 +74,7 @@ We use [Selenium](https://www.npmjs.com/package/selenium-webdriver) for cross br | |
2. Enable the 'Allow Remote Automation' option in Safari's Develop menu | ||
|
||
#### Setup Tests of the OAuth Flow | ||
By default tests mock the Firebase Authentication layer (to run faster and not require storing Google account credentials). To test with a real Google account, run tests with `REAL_LOGIN=true npm test` and the tests will pause and open a browser window for you to login with a google account in. You can provide an account email (`[email protected]`) and password (`TEST_PASSWORD=xxxx`) in the `.env` file to attempt automatic login for these test, but they may still require manual input during the login phase if your account has MFA enabled or other security settings that interfere. | ||
By default tests mock the Firebase Authentication layer (to run faster and not require storing Google account credentials). To test with a real Google account, run tests with an account email (`[email protected]`) and password (`TEST_PASSWORD=xxxx`) in the `.env` file. The tests will attempt to automatically login for these test, but they may still require manual input during the login phase if your account has MFA enabled or other security settings that interfere. Currently only supported for Google Chrome. | ||
|
||
## Glitch Development | ||
Preferably don't edit directly on Glitch except to change the production `.data` or `.env`. If necessary, | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,150 @@ | ||
/* node:coverage disable */ | ||
const {Builder, Browser, By, Key, until} = require('selenium-webdriver'); | ||
|
||
(async function example() { | ||
let driver = await new Builder() | ||
.forBrowser(Browser.CHROME) | ||
.build(); | ||
try { | ||
await driver.get('https://www.google.com/ncr'); | ||
await driver.findElement(By.name('q')).sendKeys('webdriver', Key.RETURN); | ||
await driver.wait(until.titleIs('webdriver - Google Search'), 1000); | ||
} finally { | ||
await driver.quit(); | ||
// import test utils | ||
const { describe, it, after, afterEach, before, beforeEach } = require('node:test'); // read about the builtin Node.js test framework here: https://nodejs.org/docs/latest-v18.x/api/test.html | ||
const assert = require('node:assert'); | ||
const { Builder, Browser, By, Key, until, WebDriver } = require('selenium-webdriver'); // read about selenium here: https://www.selenium.dev/documentation/en/webdriver/ | ||
const chrome = require('selenium-webdriver/chrome'); // read about chrome options here: https://chromedriver.chromium.org/capabilities | ||
const { captureConsole } = require('./utils.js'); | ||
captureConsole('./test.client.log'); | ||
|
||
describe('Client', () => { | ||
/** @type {number} */ | ||
let port; | ||
|
||
/** @type {import('node:http2').Http2Server} */ | ||
let listener; | ||
|
||
/** @type {WebDriver} */ | ||
let driver; | ||
|
||
/** @type {number} How many ms to wait for automated browser actions before failing */ | ||
const TIMEOUT = 2000; | ||
|
||
function supportsGoogleSignIn() { | ||
return process.env.TEST_EMAIL && process.env.TEST_PASSWORD && [undefined, "chrome"].includes(process.env.SELENIUM_BROWSER); | ||
} | ||
|
||
async function findDeep(path, element = null) { | ||
if (!element) { | ||
element = driver; | ||
} | ||
const getShadow = "return arguments[0].shadowRoot;"; | ||
const parts = path.split('->'); | ||
for (let i = 0; i < parts.length; i++) { | ||
try { | ||
element = await element.findElement(By.css(parts[i])); | ||
} catch (e) { | ||
const shadowRoot = await driver.executeScript(getShadow, element); | ||
element = await shadowRoot.findElement(By.css(parts[i])); | ||
} | ||
} | ||
return element; | ||
} | ||
})(); | ||
|
||
before(async () => { | ||
// spin up the server | ||
process.env.DEVELOPMENT = supportsGoogleSignIn() ? 'false' : 'true'; | ||
({ listener } = require('../server.js')); | ||
port = listener.address().port; | ||
|
||
// spin up the browser | ||
const chromeOptions = new chrome.Options(); | ||
if (supportsGoogleSignIn()) { | ||
chromeOptions.addArguments('--disable-blink-features=AutomationControlled'); | ||
chromeOptions.addArguments('--excludeSwitches=enable-automation'); | ||
chromeOptions.addArguments('--useAutomationExtension=false'); | ||
} | ||
|
||
driver = await new Builder() | ||
.forBrowser(Browser.CHROME) | ||
.setChromeOptions(chromeOptions) | ||
.build(); | ||
}); | ||
|
||
describe('Authentication', () => { | ||
it('Should signin and redirect correctly via dev login', { skip: supportsGoogleSignIn() }, async () => { | ||
await driver.get(`http://localhost:${port}/login.html?redirect=http://localhost:${port}`); | ||
await driver.findElement(By.id('signInAsAlex')).click(); | ||
await driver.wait(until.titleIs('Home'), TIMEOUT); | ||
|
||
// if login worked, trying to login again should redirect directly | ||
await driver.get(`http://localhost:${port}/login.html?redirect=http://localhost:${port}`); | ||
await driver.wait(until.titleIs('Home'), TIMEOUT); | ||
}); | ||
|
||
it('Should not show dev login in prod', { todo: true, skip: true }, async () => { | ||
// does not currently work because the client uses the hostname to determine if it's in development | ||
await driver.get(`http://localhost:${port}/login.html?redirect=http://localhost:${port}`); | ||
const signInAsAlex = await driver.findElement(By.id('signInAsAlex')); | ||
assert.strictEqual(await signInAsAlex.isDisplayed(), false); | ||
}); | ||
|
||
it('Should signin and redirect correctly via prod login', { skip: !supportsGoogleSignIn() }, async () => { | ||
// navigate to the login page | ||
await driver.get(`http://localhost:${port}/login.html?redirect=http://localhost:${port}`); | ||
// sign in with google | ||
await driver.findElement(By.id('signInWithGoogle')).click(); | ||
// wait for the popup to open | ||
await driver.wait(async () => (await driver.getAllWindowHandles()).length === 2, TIMEOUT); | ||
// figure out which window is the popup | ||
const handles = await driver.getAllWindowHandles(); | ||
const main_handle = await driver.getWindowHandle(); | ||
const popup_handle = handles.filter(handle => handle !== main_handle)[0]; | ||
// switch to the popup | ||
await driver.switchTo().window(popup_handle); | ||
// wait for the popup to load | ||
await driver.wait(until.titleIs('Sign in - Google Accounts'), TIMEOUT); | ||
await driver.sleep(100); | ||
// enter the email | ||
await driver.actions() | ||
.sendKeys(process.env.TEST_EMAIL) | ||
.sendKeys(Key.RETURN) | ||
.perform(); | ||
// wait for the password page to load | ||
await driver.sleep(3000); | ||
// enter the password | ||
await driver.actions() | ||
.sendKeys(process.env.TEST_PASSWORD) | ||
.sendKeys(Key.RETURN) | ||
.perform(); | ||
// wait for the popup to close | ||
await driver.wait(async () => (await driver.getAllWindowHandles()).length === 1, 60_000); | ||
// switch back to the main window | ||
await driver.switchTo().window(main_handle); | ||
await driver.wait(until.titleIs('Home'), TIMEOUT); | ||
// if login worked, trying to login again should redirect directly | ||
await driver.get(`http://localhost:${port}/login.html?redirect=http://localhost:${port}`); | ||
await driver.wait(until.titleIs('Home'), TIMEOUT); | ||
}); | ||
|
||
it('Signout should work', async () => { | ||
await driver.get(`http://localhost:${port}`); | ||
await driver.sleep(1000); | ||
const user_icon = await findDeep("navigation-manager->navigation-bar->user-icon"); | ||
await user_icon.click(); | ||
const profile = await findDeep("#profile", user_icon); | ||
await driver.wait(until.elementIsVisible(profile), TIMEOUT); | ||
assert.strictEqual(await profile.isDisplayed(), true); | ||
const logout = await findDeep("button:nth-of-type(3)", profile); | ||
await logout.click(); | ||
|
||
// if logout worked, trying to login again should stay on the login page | ||
await driver.get(`http://localhost:${port}/login.html?redirect=http://localhost:${port}`); | ||
await driver.wait(until.titleIs('Login'), TIMEOUT); | ||
try { | ||
await driver.wait(until.titleIs('Home'), TIMEOUT); | ||
assert.fail('Should not redirect to home page after logout'); | ||
} catch (e) { | ||
assert.strictEqual(JSON.stringify(e), "{\"name\":\"TimeoutError\",\"remoteStacktrace\":\"\"}"); | ||
} | ||
}); | ||
}); | ||
|
||
after(async () => { | ||
// close the browser | ||
await driver.quit(); | ||
|
||
// close the server | ||
await new Promise((resolve, reject) => listener.close(resolve)); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
/* node:coverage disable */ | ||
// Capture console log output in a separate file so it doesn't conflict with test output. | ||
const { createWriteStream } = require('fs'); | ||
|
||
module.exports.captureConsole = (filePath = './test.log') => { | ||
const clear = createWriteStream(filePath, {flags: 'w'}) | ||
clear.write('') | ||
clear.close(); | ||
const tty = createWriteStream(filePath, {flags: 'a'}); | ||
console.log = async (...messages) => { | ||
for (const message of messages) { | ||
const msg = typeof message === 'string' ? message : JSON.stringify(message, null, 2); | ||
tty.write(msg + '\n'); | ||
} | ||
} | ||
console.error = console.log; | ||
console.log('# Test logs created on ' + new Date().toISOString()); | ||
}; |