homebridge-gsh
Version:
Google Smart Home
721 lines (610 loc) • 27.2 kB
text/typescript
/* eslint-disable no-console */
import fs from 'fs';
import path from 'path';
import { Builder, By, until, WebDriver } from 'selenium-webdriver';
import chrome from 'selenium-webdriver/chrome';
import dotenv from 'dotenv';
const envPath = '../homebridge-gsh-server/lightsail/installStack/.env.clone-gsh.homebridge.ca';
// Load environment variables from .env
dotenv.config({ path: envPath });
let driver: WebDriver;
const describeIf = (condition: boolean, ...args: Parameters<typeof describe>) =>
condition ? describe(...args) : describe.skip(...args);
const testIf = (condition: boolean, ...args: Parameters<typeof test>) =>
condition ? test(...args) : test.skip(...args);
let cancelCreateTests = false;
let cancelCancelTests = false;
const trace = true;
describe('Prepare Environment', () => {
test.skip('should clear the Google Smart Home token in config.json', () => {
const configPath = path.resolve(process.cwd(), 'test/hbConfig/config.json');
// console.log('Config path:', configPath);
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
const gsh = config.platforms?.find((p: any) => p.platform === 'google-smarthome');
if (gsh) {
gsh.token = '';
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
// console.log('✅ Cleared token in config.json');
} else {
throw new Error('❌ Google Smart Home platform not found in config.json');
}
expect(gsh).toBeDefined();
});
});
beforeEach(() => {
checkCancel();
});
beforeAll(async () => {
const userProfileDir = path.resolve(process.cwd(), 'chrome-profile');
const options = new chrome.Options();
options.addArguments(
`--user-data-dir=${userProfileDir}`,
'--profile-directory=Default',
'--no-sandbox',
'--disable-dev-shm-usage',
'--disable-blink-features=AutomationControlled',
'--disable-infobars',
// '--start-maximized',
'--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36',
);
options.excludeSwitches('enable-automation');
options.setUserPreferences({
'profile.default_content_setting_values.notifications': 2,
'credentials_enable_service': false,
});
driver = await new Builder().forBrowser('chrome').setChromeOptions(options).build();
// Clear cookies and local storage
await driver.get('https://clone-gsh.homebridge.ca');
await driver.manage().deleteAllCookies();
await driver.executeScript('window.localStorage.clear(); window.sessionStorage.clear();');
await driver.get('http://localhost:8581/plugins');
});
afterAll(async () => {
await driver.quit();
});
async function openPluginConfig(driver: WebDriver) {
try {
const closeBtn = await driver.findElement(By.css('.modal .btn-close'));
if (await closeBtn.isDisplayed()) {
await closeBtn.click();
await driver.wait(until.stalenessOf(closeBtn), 3000);
}
} catch (e) {
console.error('Error closing modal:', e);
}
const dropdownToggle = await driver.wait(
until.elementLocated(By.css('a[ngbdropdowntoggle].dropdown-toggle')),
5000,
);
await driver.wait(until.elementIsVisible(dropdownToggle), 5000);
await dropdownToggle.click();
const pluginConfigButton = await driver.wait(
until.elementLocated(By.xpath('//button[contains(@class, \'dropdown-item\') and contains(normalize-space(), \'Plugin Config\')]')),
5000,
);
await driver.wait(until.elementIsVisible(pluginConfigButton), 5000);
await pluginConfigButton.click();
const modalTitle = await driver.wait(
until.elementLocated(By.css('.modal-title')),
5000,
);
const text = await modalTitle.getText();
expect(text).toBe('Homebridge Google Smart Home');
}
describe('Plugin Config', () => {
test('Ready for Testing', () => {
if (process.env.PAYPAL_PER_USERNAME === undefined || process.env.PAYPAL_PER_PASSWORD === undefined) {
const cancelCreateTests = true;
const cancelCancelTests = true;
}
expect(process.env.PAYPAL_PER_USERNAME).toBeDefined();
expect(process.env.PAYPAL_PER_PASSWORD).toBeDefined();
});
describe.skip('New User', () => {
test('should load NEWUSER.md content in iframe', async () => {
await openPluginConfig(driver);
const iframe = await driver.findElement(By.css('.modal-body iframe'));
await driver.switchTo().frame(iframe);
const body = await driver.findElement(By.css('body'));
const text = await body.getText();
// eslint-disable-next-line max-len
expect(text).toContain('The Homebridge Google Smart Home plugin allows you to control your Homebridge accessories from a Google Home enabled smart speaker or the Google Home mobile app');
await driver.switchTo().defaultContent();
});
describe('Account Linking', () => {
describe('when clicking Link Account', () => {
let originalWindow: string;
let popupWindow: string;
beforeAll(async () => {
// Clear the Google Smart Home token in config.json
const configPath = path.resolve(process.cwd(), 'test/hbConfig/config.json');
// console.log('Config path:', configPath);
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
const gsh = config.platforms?.find((p: any) => p.platform === 'google-smarthome');
if (gsh) {
gsh.token = '';
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
// console.log('✅ Cleared token in config.json');
} else {
throw new Error('❌ Google Smart Home platform not found in config.json');
}
expect(gsh).toBeDefined();
await openPluginConfig(driver);
originalWindow = await driver.getWindowHandle();
const iframe = await driver.findElement(By.css('.modal-body iframe'));
await driver.switchTo().frame(iframe);
const linkBtn = await driver.findElement(By.xpath('//button[contains(text(), \'Link Account\')]'));
await linkBtn.click();
await driver.wait(async () => {
const handles = await driver.getAllWindowHandles();
return handles.length > 1;
}, 10000);
const handles = await driver.getAllWindowHandles();
popupWindow = handles.find(h => h !== originalWindow)!;
});
afterAll(async () => {
const handles = await driver.getAllWindowHandles();
if (handles.includes(popupWindow)) {
await driver.switchTo().window(popupWindow);
await driver.close();
}
await driver.switchTo().window(originalWindow);
});
test('should open a new popup window', async () => {
expect(popupWindow).toBeDefined();
});
test('should redirect to Auth0', async () => {
await safeSwitchToWindow(popupWindow);
console.log('-1 Current URL:', await driver.getCurrentUrl());
// await driver.wait(until.urlContains('https://clone-gsh.homebridge.ca/link-account'));
await driver.wait(until.urlContains('auth0.com'));
const url = await driver.getCurrentUrl();
expect(url).toContain('auth0.com');
expect(url).not.toContain('https://clone-gsh.homebridge.ca/link-account');
});
test('should click the Log in with Google button', async () => {
await safeSwitchToWindow(popupWindow);
console.log('1 Current URL:', await driver.getCurrentUrl());
const googleBtn = await driver.wait(
until.elementLocated(By.css('button[data-provider="google-oauth2"]')),
);
await driver.wait(until.elementIsVisible(googleBtn));
console.log('2 Current URL:', await driver.getCurrentUrl());
const googleText = await googleBtn.getText();
console.log('3 Google login button text:', googleText);
expect(googleText).toContain('LOG IN WITH GOOGLE');
await googleBtn.click();
const url = await driver.getCurrentUrl();
console.log('Redirected URL after click:', url);
expect(url).toContain('https://accounts.google.com/v3/signin');
}, 20000);
test('should enter Google login credentials', async () => {
await safeSwitchToWindow(popupWindow);
console.log('4 Current URL:', await driver.getCurrentUrl());
const emailInput = await driver.wait(
until.elementLocated(By.id('identifierId')),
);
await emailInput.clear();
await emailInput.sendKeys(process.env.GOOGLE_USERNAME);
console.log('5 Current URL:', await driver.getCurrentUrl());
await driver.findElement(By.id('identifierNext')).click();
console.log('6 Current URL:', await driver.getCurrentUrl());
// const html = await driver.getPageSource();
// fs.writeFileSync('test/hbConfig/google-login.html', html);
// console.log(html);
const passwordInput = await driver.wait(
until.elementLocated(By.name('password')),
);
console.log('7 Current URL:', await driver.getCurrentUrl());
await passwordInput.clear();
await passwordInput.sendKeys(process.env.GOOGLE_PASSWORD);
console.log('8 Current URL:', await driver.getCurrentUrl());
await driver.findElement(By.id('passwordNext')).click();
});
// No need for actual login, browser used stored credentials
test('Confirm account linking', async () => {
await safeSwitchToWindow(popupWindow);
const confirmButton = await driver.wait(
until.elementLocated(By.xpath('//button[contains(text(), \'Confirm\')]')),
);
console.log('a Current URL:', await driver.getCurrentUrl());
await driver.wait(until.elementIsVisible(confirmButton));
console.log('b Current URL:', await driver.getCurrentUrl());
await confirmButton.click();
console.log('c Current URL:', await driver.getCurrentUrl());
await driver.wait(async () => {
const handles = await driver.getAllWindowHandles();
return !handles.includes(popupWindow);
});
console.log('d Popup window closed');
const remainingHandles = await driver.getAllWindowHandles();
console.log('e Popup window closed');
expect(remainingHandles).not.toContain(popupWindow);
});
test('should confirm the popup window has closed', async () => {
const handles = await driver.getAllWindowHandles();
// Optional debug
console.log('223: Remaining window handles:', handles);
// Expect only the main window to remain
expect(handles.length).toBe(1);
await safeSwitchToWindow(originalWindow);
});
});
test('sleep 10 seconds to observe the popup', async () => {
console.log('Sleeping for 10 seconds to observe the popup...');
await driver.sleep(120000);
}, 121000);
});
});
describe('Create Subscription', () => {
let originalWindow: string;
let popupWindow: string;
beforeAll(async () => {
await openPluginConfig(driver);
});
test('should show Account Status as Trial with expiry', async () => {
console.log('256: AS Current URL:', await driver.getCurrentUrl());
const iframe = await driver.findElement(By.css('.modal-body iframe'));
await driver.switchTo().frame(iframe);
const statusElement = await driver.wait(
until.elementLocated(By.xpath('//p[contains(., \'Account Status:\')]')),
5000,
);
const statusText = await statusElement.getText();
console.log('265: Account status text:', statusText);
if (!statusText.includes('Trial')) {
cancelCreateTests = true;
console.log('268: Canceling all tests due to Trial status');
}
expect(statusText).toMatch(/Account Status: Trial, Expiry: \d{1,2} \w{3} 20\d{2}/); // ✅ RegExp
originalWindow = await driver.getWindowHandle();
await driver.switchTo().defaultContent();
});
test('should expand the Create Subscription section', async () => {
const iframe = await driver.findElement(By.css('.modal-body iframe'));
await driver.switchTo().frame(iframe);
// Click the legend
const legend = await driver.wait(
until.elementLocated(By.xpath('//legend[contains(normalize-space(), \'Create Subscription\')]')),
5000,
);
expect(legend).toBeDefined();
await legend.click();
await driver.switchTo().defaultContent();
});
test('should click the PayPal button in first container', async () => {
const iframe = await driver.findElement(By.css('.modal-body iframe'));
await driver.switchTo().frame(iframe);
const paypalContainer = await driver.findElement(By.id('paypal-button-container-0'));
// console.log('297: 💡 Dumping page source before wait...');
// const html = await driver.getPageSource();
// console.log(html);
const paypalIframe = await driver.wait(
until.elementLocated(By.css('#paypal-button-container-0 iframe.component-frame')),
10000, // wait up to 10s
);
await driver.switchTo().frame(paypalIframe);
const paypalButton = await driver.wait(
until.elementLocated(By.css('div.paypal-button[data-funding-source="paypal"]')),
10000,
);
expect(paypalButton).toBeDefined();
await driver.wait(until.elementIsVisible(paypalButton), 5000);
await paypalButton.click();
console.log('296: PayPal button clicked.');
await driver.switchTo().defaultContent();
});
test('Enter PayPal login credentials in popup', async () => {
// Wait for popup to open
await driver.wait(async () => (await driver.getAllWindowHandles()).length > 1, 10000);
const handles = await driver.getAllWindowHandles();
const popupHandle = handles.find(h => h !== handles[0])!;
await driver.switchTo().window(popupHandle);
await driver.wait(until.titleIs('Log in to your PayPal account'));
expect(await driver.getTitle()).toContain('Log in to your PayPal account');
if (trace) {
console.log('314: ', await driver.getTitle());
}
// expect(await driver.findElement(By.css("body")).getText()).toContain('Subscription Options');
if (trace) {
console.log('316: ', await driver.findElement(By.css('body')).getText());
}
// expect(await driver.findElement(By.css("body")).getText()).toContain('Pay with PayPal');
const emailInput = await driver.findElement(By.id('email'));
await emailInput.clear();
await emailInput.sendKeys(process.env.PAYPAL_PER_USERNAME);
if (trace) {
console.log('323: ', await driver.findElement(By.css('body')).getText());
}
//await driver.findElement(By.id('btnNext')).click();
try {
const nextButton = await driver.findElement(By.id('btnNext'));
await nextButton.click();
if (trace) {
console.log('400: Clicked Next button');
}
// Optionally wait for password field to be present
await driver.wait(until.elementLocated(By.id('password')), 5000);
} catch (err) {
if (trace) {
console.log('Next button not shown, skipping to password entry');
}
}
// sleep(1000);
if (trace) {
console.log('327: ', await driver.getTitle());
}
if (trace) {
console.log('328: ', await driver.findElement(By.css('body')).getText());
}
//await driver.wait(until.titleIs('Log in to your PayPal account'));
//expect(await driver.findElement(By.css("body")).getText()).toContain('Pay with PayPal');
await driver.findElement(By.id('password')).sendKeys(process.env.PAYPAL_PER_PASSWORD);
console.log('421: password entered');
await driver.findElement(By.id('btnLogin')).click();
console.log('423: password entered');
await driver.wait(until.titleIs('PayPal Checkout - Choose a way to pay'));
console.log('425: Choose a way to pay');
expect(await driver.getTitle()).toContain('PayPal Checkout - Choose a way to pay');
if (trace) {
console.log('341: ', await driver.getTitle());
}
if (trace) {
console.log('342: ', await driver.findElement(By.css('body')).getText());
}
// expect(await driver.findElement(By.css("body")).getText()).toContain('Subscription Options');
await driver.findElement(By.xpath('//button[contains(@ng-click, \'continue()\')]')).click();
await driver.wait(until.titleIs('PayPal Checkout - Review your payment'));
expect(await driver.getTitle()).toContain('PayPal Checkout - Review your payment');
// sleep(1000);
if (trace) {
console.log('334', await driver.getTitle());
}
if (trace) {
console.log('335', await driver.findElement(By.css('body')).getText());
}
// expect(await driver.findElement(By.css("body")).getText()).toContain('Subscription Options');
if ((await driver.getTitle()) === 'PayPal Checkout - Choose a way to pay') {
if (trace) {
console.log('339 - Clicking continue', await driver.getTitle());
}
await driver.findElement(By.xpath('//button[contains(@ng-click, \'continue()\')]')).click();
}
// await driver.wait(until.titleIs('PayPal Checkout - Review your payment'));
// sleep(1000);
if (trace) {
console.log('344', await driver.getTitle());
}
const confirmButton = await driver.wait(
until.elementLocated(By.id('confirmButtonTop')),
5000,
);
if (trace) {
console.log('345', await driver.findElement(By.css('body')).getText());
}
expect(confirmButton).toBeDefined();
await driver.findElement(By.id('confirmButtonTop')).click();
if (trace) {
console.log('348', await driver.getTitle());
}
await driver.wait(until.titleIs('PayPal Checkout - Review your payment'));
if (trace) {
console.log('350', await driver.getTitle());
}
expect(await driver.getTitle()).toContain('PayPal Checkout - Review your payment');
// sleep(1000);
if (trace) {
console.log('358', await driver.getTitle());
}
if (trace) {
console.log('359', await driver.findElement(By.css('body')).getText());
}
await driver.wait(async () => {
const handles = await driver.getAllWindowHandles();
return handles.length === 1;
}, 10000);
await safeSwitchToWindow(originalWindow);
if (trace) {
console.log('362: ', await driver.getTitle());
}
expect(await driver.getTitle()).toContain('HB GSH Test');
// await driver.wait(until.elementLocated(By.id('notification')));
// Click the legend
// console.log('💡 Dumping page source before wait...');
// const html = await driver.getPageSource();
// console.log(html);
await driver.wait(
until.elementLocated(By.xpath('//div[contains(@class, \'toast-message\') and contains(text(), \'Service Subscription Created\')]')),
10000,
);
if (trace) {
console.log('388: ', await driver.getTitle());
}
// Wait for the toast notification to appear
/*
await driver.wait(
until.elementLocated(By.id('toast-container')),
3000
);
const toast = await driver.wait(
until.elementLocated(By.css('#toast-container .toast')),
7000
);
expect(toast).toBeDefined();
const toastText = await toast.getText();
expect(toastText).toBeDefined();
// Assert the toast message confirms cancellation
expect(toastText.toLowerCase()).toContain('cancelled');
*/
if (trace) {
console.log('364', await driver.getTitle());
}
// sleep(1000);
}, 30000);
test('should confirm the popup window has closed', async () => {
const handles = await driver.getAllWindowHandles();
// Optional debug
console.log('399: Remaining window handles:', handles);
// Expect only the main window to remain
expect(handles.length).toBe(1);
await safeSwitchToWindow(originalWindow);
});
test('should show Account Status as Subscription:', async () => {
if (trace) {
console.log('407', await driver.getTitle());
}
expect(await driver.getTitle()).toContain('HB GSH Test');
const iframe = await driver.findElement(By.css('.modal-body iframe'));
await driver.switchTo().frame(iframe);
// Wait for the paypal buttons to disappear
await driver.wait(async () => {
const frames = await driver.findElements(By.css('iframe.component-frame'));
return frames.length === 0;
}, 10000); // wait up to 10s
const statusElement = await driver.wait(
until.elementLocated(By.xpath('//p[contains(., \'Account Status:\')]')),
5000,
);
// console.log('💡 Dumping page source before wait...');
// const html = await driver.getPageSource();
// console.log(html);
const statusText = await statusElement.getText();
console.log('417: Account status text:', statusText);
if (statusText.includes('Trial')) {
// cancelAllTests = true;
console.log('448: Canceling all tests due to Trial status');
}
expect(statusText).toMatch(/Account Status: Subscription: Euro Monthly/); // ✅ RegExp
originalWindow = await driver.getWindowHandle();
await driver.switchTo().defaultContent();
});
test('sleep 10 seconds to observe the popup', async () => {
console.log('Sleeping for 10 seconds to observe the popup...');
await driver.sleep(10000);
}, 121000);
});
describe('Manage Subscription', () => {
let originalWindow: string;
let popupWindow: string;
beforeAll(async () => {
await openPluginConfig(driver);
});
test('should show Account Status as Subscription:', async () => {
if (trace) {
console.log('440', await driver.getTitle());
}
if (await driver.getTitle() !== 'HB GSH Test') {
console.log('Canceling all tests due to incorrect title');
cancelCancelTests = true;
}
expect(await driver.getTitle()).toContain('HB GSH Test');
const iframe = await driver.findElement(By.css('.modal-body iframe'));
await driver.switchTo().frame(iframe);
const statusElement = await driver.wait(
until.elementLocated(By.xpath('//p[contains(., \'Account Status:\')]')),
5000,
);
const statusText = await statusElement.getText();
console.log('475: Account status text:', statusText);
if (statusText.includes('Trial')) {
cancelCancelTests = true;
console.log('Canceling all tests due to Trial status');
}
expect(statusText).toMatch(/Account Status: Subscription: Euro Monthly/); // ✅ RegExp
originalWindow = await driver.getWindowHandle();
await driver.switchTo().defaultContent();
});
test('should expand the Subscription Details section', async () => {
const iframe = await driver.findElement(By.css('.modal-body iframe'));
await driver.switchTo().frame(iframe);
// Click the legend
const legend = await driver.wait(
until.elementLocated(By.xpath('//legend[contains(normalize-space(), \'Subscription Details\')]')),
5000,
);
expect(legend).toBeDefined();
await legend.click();
await driver.switchTo().defaultContent();
});
test('should click the Cancel Subscription button', async () => {
const iframe = await driver.findElement(By.css('.modal-body iframe'));
await driver.switchTo().frame(iframe);
const button = await driver.wait(
until.elementLocated(By.xpath('//button[contains(text(), \'Cancel Subscription\')]')),
1000,
);
await driver.wait(until.elementIsVisible(button), 3000);
await driver.wait(until.elementIsEnabled(button), 3000);
expect(button).toBeDefined();
await button.click();
await driver.switchTo().defaultContent();
});
test('should confirm subscription cancellation', async () => {
const iframe = await driver.findElement(By.css('.modal-body iframe'));
await driver.switchTo().frame(iframe);
// Click "Yes, Cancel" in the confirm dialog
const confirmButton = await driver.wait(
until.elementLocated(By.xpath('//button[contains(text(), \'Yes, Cancel\')]')),
3000,
);
await driver.wait(until.elementIsVisible(confirmButton), 3000);
await driver.wait(until.elementIsEnabled(confirmButton), 3000);
await confirmButton.click();
// Wait for overlay to disappear
await driver.wait(
until.stalenessOf(confirmButton),
1000,
);
});
test('Validate updated status', async () => {
await driver.switchTo().defaultContent();
const iframe = await driver.findElement(By.css('.modal-body iframe'));
expect(iframe).toBeDefined();
// await driver.switchTo().frame(iframe);
// Wait for the toast notification to appear
// console.log('550: 💡 Dumping page source before wait...');
// const html = await driver.getPageSource();
// console.log(html);
// Verify updated account status
const toast = await driver.wait(
until.elementLocated(By.xpath('//div[contains(@class, \'toast-message\')]')),
10000,
);
if (trace) {
console.log('580: Toast message found', await toast.getText());
}
const toastText = await toast.getText();
expect(toastText).toBe('Subscription Cancelled');
const bodyText = await driver.findElement(By.css('body')).getText();
expect(bodyText).not.toContain('Subscription: Euro Monthly');
await driver.switchTo().defaultContent();
});
test('confirm no popup windows are open', async () => {
const handles = await driver.getAllWindowHandles();
// Optional debug
if (trace) {
console.log('590: Remaining window handles:', handles);
}
// Expect only the main window to remain
expect(handles.length).toBe(1);
await safeSwitchToWindow(originalWindow);
});
test('sleep 10 seconds to observe the popup', async () => {
console.log('Sleeping for 10 seconds to observe the popup...');
await driver.sleep(10000);
}, 11000);
});
});
// Helpers
async function safeSwitchToWindow(handle: string) {
const handles = await driver.getAllWindowHandles();
if (!handles.includes(handle)) {
throw new Error(`Window handle ${handle} no longer exists`);
}
await driver.switchTo().window(handle);
}
function checkCancel() {
if (cancelCancelTests || cancelCreateTests) {
throw new Error('Test run canceled');
}
}