diff options
Diffstat (limited to 'e2e/lib')
-rw-r--r-- | e2e/lib/Console.js | 41 | ||||
-rw-r--r-- | e2e/lib/Console.ts | 72 | ||||
-rw-r--r-- | e2e/lib/FormOptionPage.ts | 68 | ||||
-rw-r--r-- | e2e/lib/JSONOptionPage.ts | 22 | ||||
-rw-r--r-- | e2e/lib/OptionPage.ts | 39 | ||||
-rw-r--r-- | e2e/lib/Page.ts | 93 | ||||
-rw-r--r-- | e2e/lib/TestServer.ts | 64 | ||||
-rw-r--r-- | e2e/lib/clipboard.js | 63 | ||||
-rw-r--r-- | e2e/lib/clipboard.ts | 107 |
9 files changed, 465 insertions, 104 deletions
diff --git a/e2e/lib/Console.js b/e2e/lib/Console.js deleted file mode 100644 index 3a39b64..0000000 --- a/e2e/lib/Console.js +++ /dev/null @@ -1,41 +0,0 @@ -class Console { - constructor(session) { - this.session = session; - } - - async sendKeys(...keys) { - let input = await this.session.findElementByCSS('input'); - input.sendKeys(...keys); - } - - async currentValue() { - return await this.session.executeScript(() => { - let input = document.querySelector('input'); - return input.value; - }); - } - - async getCompletions() { - return await this.session.executeScript(() => { - let items = document.querySelectorAll('.vimvixen-console-completion > li'); - if (items.length === 0) { - throw new Error('completion items not found'); - } - - let objs = []; - for (let li of items) { - if (li.classList.contains('vimvixen-console-completion-title')) { - objs.push({ type: 'title', text: li.textContent.trim() }); - } else if ('vimvixen-console-completion-item') { - let highlight = li.classList.contains('vimvixen-completion-selected'); - objs.push({ type: 'item', text: li.textContent.trim(), highlight }); - } else { - throw new Error(`unexpected class: ${li.className}`); - } - } - return objs; - }); - } -} - -module.exports = Console; diff --git a/e2e/lib/Console.ts b/e2e/lib/Console.ts new file mode 100644 index 0000000..233bf48 --- /dev/null +++ b/e2e/lib/Console.ts @@ -0,0 +1,72 @@ +import { WebDriver, By, Key } from 'selenium-webdriver'; + +export type CompletionItem = { + type: string; + text: string; + highlight: boolean; +} + +export class Console { + constructor(private webdriver: WebDriver) { + } + + async sendKeys(...keys: string[]) { + let input = await this.webdriver.findElement(By.css('input')); + input.sendKeys(...keys); + } + + async currentValue() { + return await this.webdriver.executeScript(() => { + let input = document.querySelector('input'); + if (input === null) { + throw new Error('could not find input element'); + } + return input.value; + }); + } + + async execCommand(command: string): Promise<void> { + let input = await this.webdriver.findElement(By.css('input.vimvixen-console-command-input')); + await input.sendKeys(command, Key.ENTER); + } + + async getErrorMessage(): Promise<string> { + let p = await this.webdriver.findElement(By.css('.vimvixen-console-error')); + return p.getText(); + } + + async inputKeys(...keys: string[]) { + let input = await this.webdriver.findElement(By.css('input')); + await input.sendKeys(...keys); + } + + getCompletions(): Promise<CompletionItem[]> { + return this.webdriver.executeScript(() => { + let items = document.querySelectorAll('.vimvixen-console-completion > li'); + if (items.length === 0) { + throw new Error('completion items not found'); + } + + let objs = []; + for (let li of Array.from(items)) { + if (li.classList.contains('vimvixen-console-completion-title')) { + objs.push({ type: 'title', text: li.textContent!!.trim() }); + } else if ('vimvixen-console-completion-item') { + let highlight = li.classList.contains('vimvixen-completion-selected'); + objs.push({ type: 'item', text: li.textContent!!.trim(), highlight }); + } else { + throw new Error(`unexpected class: ${li.className}`); + } + } + return objs; + }); + } + + async close(): Promise<void> { + let input = await this.webdriver.findElement(By.css('input')); + await input.sendKeys(Key.ESCAPE); + // TODO remove sleep + await new Promise(resolve => setTimeout(resolve, 100)); + await (this.webdriver.switchTo() as any).parentFrame(); + } +} diff --git a/e2e/lib/FormOptionPage.ts b/e2e/lib/FormOptionPage.ts new file mode 100644 index 0000000..c49a44f --- /dev/null +++ b/e2e/lib/FormOptionPage.ts @@ -0,0 +1,68 @@ +import { Lanthan } from 'lanthan'; +import { WebDriver, By, until } from 'selenium-webdriver'; + +export default class FormOptionPage { + private webdriver: WebDriver; + + constructor(lanthan: Lanthan) { + this.webdriver = lanthan.getWebDriver(); + } + + async setBlacklist(nth: number, value: string): Promise<void> { + let selector = '.form-blacklist-form-row > .column-url'; + let inputs = await this.webdriver.findElements(By.css(selector)); + if (inputs.length <= nth) { + throw new RangeError('Index out of range to set a blacklist') + } + await inputs[nth].sendKeys(value); + await this.webdriver.executeScript(`document.querySelectorAll('${selector}')[${nth}].blur()`); + } + + async setSearchEngine(nth: number, name: string, url: string) { + let selector = '.form-search-form-row > .column-name'; + let inputs = await this.webdriver.findElements(By.css(selector)); + if (inputs.length <= nth) { + throw new RangeError('Index out of range to set a search engine') + } + await inputs[nth].sendKeys(name); + await this.webdriver.executeScript(`document.querySelectorAll('${selector}')[${nth}].blur()`); + + selector = '.form-search-form-row > .column-url'; + inputs = await this.webdriver.findElements(By.css(selector)); + if (inputs.length <= nth) { + throw new RangeError('Index out of range to set a search engine') + } + await inputs[nth].sendKeys(url); + await this.webdriver.executeScript(`document.querySelectorAll('${selector}')[${nth}].blur()`); + } + + async addBlacklist(): Promise<void> { + let rows = await this.webdriver.findElements(By.css(`.form-blacklist-form-row`)); + let button = await this.webdriver.findElement(By.css('.form-blacklist-form .ui-add-button')) + await button.click(); + await this.webdriver.wait(until.elementLocated(By.css(`.form-blacklist-form-row:nth-child(${rows.length + 1})`))); + } + + async removeBlackList(nth: number): Promise<void> { + let buttons = await this.webdriver.findElements(By.css('.form-blacklist-form-row .ui-delete-button')); + if (buttons.length <= nth) { + throw new RangeError('Index out of range to remove blacklist') + } + await buttons[nth].click() + } + + async addSearchEngine(): Promise<void> { + let rows = await this.webdriver.findElements(By.css(`.form-search-form-row > .column-name`)); + let button = await this.webdriver.findElement(By.css('.form-search-form > .ui-add-button')) + await button.click(); + await this.webdriver.wait(until.elementLocated(By.css(`.form-search-form-row:nth-child(${rows.length + 1})`))); + } + + async setDefaultSearchEngine(nth: number): Promise<void> { + let radios = await this.webdriver.findElements(By.css('.form-search-form-row input[type=radio]')); + if (radios.length <= nth) { + throw new RangeError('Index out of range to set a default search engine'); + } + await radios[nth].click(); + } +} diff --git a/e2e/lib/JSONOptionPage.ts b/e2e/lib/JSONOptionPage.ts new file mode 100644 index 0000000..ac1ae3d --- /dev/null +++ b/e2e/lib/JSONOptionPage.ts @@ -0,0 +1,22 @@ +import { Lanthan } from 'lanthan'; +import { WebDriver, By } from 'selenium-webdriver'; + +export default class JSONOptionPage { + private webdriver: WebDriver; + + constructor(lanthan: Lanthan) { + this.webdriver = lanthan.getWebDriver(); + } + + async updateSettings(value: string): Promise<void> { + let textarea = await this.webdriver.findElement(By.css('textarea')); + await this.webdriver.executeScript(`document.querySelector('textarea').value = '${value}'`) + await textarea.sendKeys(' '); + await this.webdriver.executeScript(() => document.querySelector('textarea')!!.blur()); + } + + async getErrorMessage(): Promise<string> { + let error = await this.webdriver.findElement(By.css('.settings-ui-input-error')); + return error.getText(); + } +} diff --git a/e2e/lib/OptionPage.ts b/e2e/lib/OptionPage.ts new file mode 100644 index 0000000..c183b06 --- /dev/null +++ b/e2e/lib/OptionPage.ts @@ -0,0 +1,39 @@ +import { Lanthan } from 'lanthan'; +import { WebDriver, By } from 'selenium-webdriver'; +import JSONOptionPage from './JSONOptionPage'; +import FormOptionPage from './FormOptionPage'; + +export default class OptionPage { + private webdriver: WebDriver; + + constructor(private lanthan: Lanthan) { + this.webdriver = lanthan.getWebDriver(); + } + + static async open(lanthan: Lanthan) { + let url = await lanthan.getWebExtBrowser().runtime.getURL("build/settings.html") + await lanthan.getWebDriver().navigate().to(url); + return new OptionPage(lanthan); + } + + async switchToForm(): Promise<FormOptionPage> { + let useFormInput = await this.webdriver.findElement(By.css('#setting-source-form')); + await useFormInput.click(); + await this.webdriver.switchTo().alert().accept(); + return new FormOptionPage(this.lanthan); + } + + async asFormOptionPage(): Promise<FormOptionPage> { + // TODO validate current page + return new FormOptionPage(this.lanthan); + } + + async asJSONOptionPage(): Promise<JSONOptionPage> { + // TODO validate current page + return new JSONOptionPage(this.lanthan); + } + + scrollTo(x: number, y: number): Promise<void> { + return this.webdriver.executeScript(`window.scrollTo(${x}, ${y})`); + } +} diff --git a/e2e/lib/Page.ts b/e2e/lib/Page.ts new file mode 100644 index 0000000..7a5dd7a --- /dev/null +++ b/e2e/lib/Page.ts @@ -0,0 +1,93 @@ +import { WebDriver, By, until } from 'selenium-webdriver'; +import { Console } from './Console'; + +type Hint = { + displayed: boolean, + text: string, +}; + +export default class Page { + private constructor(private webdriver: WebDriver) { + } + + static async currentContext(webdriver: WebDriver): Promise<Page> { + await Page.waitForConsoleLoaded(webdriver); + return new Page(webdriver); + } + + static async navigateTo(webdriver: WebDriver, url: string): Promise<Page> { + await webdriver.navigate().to(url); + await Page.waitForConsoleLoaded(webdriver); + return new Page(webdriver); + } + + async sendKeys(...keys: Array<string|number|Promise<string|number>>): Promise<void> { + let body = await this.webdriver.findElement(By.css('body')); + await body.sendKeys(...keys); + } + + async navigateTo(url: string): Promise<Page> { + await this.webdriver.navigate().to(url); + await Page.waitForConsoleLoaded(this.webdriver); + return new Page(this.webdriver); + } + + async showConsole(): Promise<Console> { + let iframe = this.webdriver.findElement(By.css('#vimvixen-console-frame')); + + await this.sendKeys(':'); + await this.webdriver.wait(until.elementIsVisible(iframe)); + await this.webdriver.switchTo().frame(0); + await this.webdriver.wait(until.elementLocated(By.css('input.vimvixen-console-command-input'))); + return new Console(this.webdriver); + } + + async getConsole(): Promise<Console> { + let iframe = this.webdriver.findElement(By.css('#vimvixen-console-frame')); + + await this.webdriver.wait(until.elementIsVisible(iframe)); + await this.webdriver.switchTo().frame(0); + return new Console(this.webdriver); + } + + async getScrollX(): Promise<number> { + return await this.webdriver.executeScript(() => window.pageXOffset); + } + + getScrollY(): Promise<number> { + return this.webdriver.executeScript(() => window.pageYOffset); + } + + scrollTo(x: number, y: number): Promise<void> { + return this.webdriver.executeScript(`window.scrollTo(${x}, ${y})`); + } + + pageHeight(): Promise<number> { + return this.webdriver.executeScript(() => window.document.documentElement.clientHeight); + } + + async waitAndGetHints(): Promise<Hint[]> { + await this.webdriver.wait(until.elementsLocated(By.css('.vimvixen-hint'))); + + let elements = await this.webdriver.findElements(By.css(`.vimvixen-hint`)); + let hints = []; + for (let e of elements) { + let display = await e.getCssValue('display'); + let text = await e.getText(); + hints.push({ + displayed: display !== 'none', + text: text, + }); + } + return hints; + } + + private static async waitForConsoleLoaded(webdriver: WebDriver) { + let topFrame = await webdriver.executeScript(() => window.top === window); + if (!topFrame) { + return; + } + await webdriver.wait(until.elementLocated(By.css('iframe.vimvixen-console-frame'))); + await new Promise(resolve => setTimeout(resolve, 100)); + } +} diff --git a/e2e/lib/TestServer.ts b/e2e/lib/TestServer.ts new file mode 100644 index 0000000..c010e37 --- /dev/null +++ b/e2e/lib/TestServer.ts @@ -0,0 +1,64 @@ +import * as http from 'http'; +import * as net from 'net' +import express from 'express'; + +type HandlerFunc = (req: express.Request, res: express.Response) => void; + +export default class TestServer { + private http?: http.Server; + + private app: express.Application; + + constructor( + private port = 0, + private address = '127.0.0.1', + ){ + this.app = express(); + } + + handle(path: string, f: HandlerFunc): TestServer { + this.app.get(path, f); + return this; + } + + receiveContent(path: string, content: string): TestServer { + this.app.get(path, (_req: express.Request, res: express.Response) => { + res.status(200).send(content) + }); + return this; + } + + url(path: string = '/'): string { + if (!this.http) { + throw new Error('http server not started'); + } + + let addr = this.http.address() as net.AddressInfo; + return `http://${addr.address}:${addr.port}${path}` + } + + start(): Promise<void> { + if (this.http) { + throw new Error('http server already started'); + } + + this.http = http.createServer(this.app) + return new Promise((resolve) => { + this.http!!.listen(this.port, this.address, () => { + resolve(); + }) + }); + } + + stop(): Promise<void> { + if (!this.http) { + return Promise.resolve(); + } + return new Promise((resolve) => { + this.http!!.close(() => { + this.http = undefined; + resolve(); + }); + }) + } +} diff --git a/e2e/lib/clipboard.js b/e2e/lib/clipboard.js deleted file mode 100644 index 4061dbd..0000000 --- a/e2e/lib/clipboard.js +++ /dev/null @@ -1,63 +0,0 @@ -'use strict'; - -const { spawn } = require('child_process'); - -const readLinux = () => { - let stdout = '', stderr = ''; - return new Promise((resolve, reject) => { - let xsel = spawn('xsel', ['--clipboard', '--output']); - xsel.stdout.on('data', (data) => { - stdout += data; - }); - xsel.stderr.on('data', (data) => { - stderr += data; - }); - xsel.on('close', (code) => { - if (code !== 0) { - throw new Error(`xsel returns ${code}: ${stderr}`) - } - resolve(stdout); - }); - }); -}; - -const writeLinux = (data) => { - let stdout = '', stderr = ''; - return new Promise((resolve, reject) => { - let xsel = spawn('xsel', ['--clipboard', '--input']); - xsel.stderr.on('data', (data) => { - stderr += data; - }); - xsel.on('close', (code) => { - if (code !== 0) { - throw new Error(`xsel returns ${code}: ${stderr}`) - } - resolve(); - }); - xsel.stdin.write(data); - xsel.stdin.end(); - }); -}; - -const unsupported = (os) => { - return () => { - throw new Error(`Unsupported os: ${os}`); - }; -}; - -const detect = () => { - switch (process.platform) { - case 'linux': - return { - read: readLinux, - write: writeLinux, - }; - default: - return { - read: unsupported(process.platform), - write: unsupported(process.platform), - }; - } -} - -module.exports = detect(); diff --git a/e2e/lib/clipboard.ts b/e2e/lib/clipboard.ts new file mode 100644 index 0000000..c1eddbb --- /dev/null +++ b/e2e/lib/clipboard.ts @@ -0,0 +1,107 @@ +import { spawn } from 'child_process'; + +const readLinux = (): Promise<string> => { + let stdout = '', stderr = ''; + return new Promise((resolve) => { + let xsel = spawn('xsel', ['--clipboard', '--output']); + xsel.stdout.on('data', (data) => { + stdout += data; + }); + xsel.stderr.on('data', (data) => { + stderr += data; + }); + xsel.on('close', (code) => { + if (code !== 0) { + throw new Error(`xsel returns ${code}: ${stderr}`) + } + resolve(stdout); + }); + }); +}; + +const writeLinux = (data: string): Promise<string> => { + let stderr = ''; + return new Promise((resolve) => { + let xsel = spawn('xsel', ['--clipboard', '--input']); + xsel.stderr.on('data', (data) => { + stderr += data; + }); + xsel.on('close', (code) => { + if (code !== 0) { + throw new Error(`xsel returns ${code}: ${stderr}`) + } + resolve(); + }); + xsel.stdin.write(data); + xsel.stdin.end(); + }); +}; + +const readDarwin = (): Promise<string> => { + let stdout = '', stderr = ''; + return new Promise((resolve) => { + let pbpaste = spawn('pbpaste'); + pbpaste.stdout.on('data', (data) => { + stdout += data; + }); + pbpaste.stderr.on('data', (data) => { + stderr += data; + }); + pbpaste.on('close', (code) => { + if (code !== 0) { + throw new Error(`pbpaste returns ${code}: ${stderr}`) + } + resolve(stdout); + }); + }); +}; + +const writeDarwin = (data: string): Promise<string> => { + let stderr = ''; + return new Promise((resolve) => { + let pbcopy = spawn('pbcopy'); + pbcopy.stderr.on('data', (data) => { + stderr += data; + }); + pbcopy.on('close', (code) => { + if (code !== 0) { + throw new Error(`pbcopy returns ${code}: ${stderr}`) + } + resolve(); + }); + pbcopy.stdin.write(data); + pbcopy.stdin.end(); + }); +}; + +class UnsupportedError extends Error { + constructor(platform: string) { + super(); + this.message = `Unsupported platform: ${platform}`; + } +} + +const read = () => { + switch (process.platform) { + case 'linux': + return readLinux(); + case 'darwin': + return readDarwin(); + } + throw new UnsupportedError(process.platform); +} + +const write = (data: string) => { + switch (process.platform) { + case 'linux': + return writeLinux(data); + case 'darwin': + return writeDarwin(data); + } + throw new UnsupportedError(process.platform); +} + +export { + read, + write, +}; |