diff --git a/examples/javascript/test/drivers/gridServer.js b/examples/javascript/test/drivers/gridServer.js new file mode 100644 index 000000000000..3d617996498b --- /dev/null +++ b/examples/javascript/test/drivers/gridServer.js @@ -0,0 +1,91 @@ +const { spawn } = require('node:child_process') +const http = require('node:http') +const path = require('node:path') +const portprober = require('selenium-webdriver/net/portprober') + +const SERVER_JAR = path.join(__dirname, '..', '..', '..', 'selenium-server-4.49.0.jar') + +function waitForServer(url, timeout) { + const deadline = Date.now() + timeout + + return new Promise((resolve, reject) => { + function retryOrFail() { + if (Date.now() > deadline) { + reject(new Error(`Selenium server did not start within ${timeout}ms`)) + return + } + setTimeout(attempt, 200) + } + + function attempt() { + let settled = false + const retryOnce = () => { + if (settled) return + settled = true + retryOrFail() + } + + const req = http.get(url, { timeout: 2000 }, (res) => { + let body = '' + res.on('data', (chunk) => (body += chunk)) + res.on('error', retryOnce) + res.on('aborted', retryOnce) + res.on('end', () => { + if (settled) return + try { + const { value } = JSON.parse(body) + if (res.statusCode === 200 && value && value.ready) { + settled = true + resolve() + return + } + } catch (err) { + // Not valid JSON yet (e.g. server still booting) - fall through to retry. + } + retryOnce() + }) + }) + req.on('timeout', () => { + req.destroy() + retryOnce() + }) + req.on('error', retryOnce) + } + attempt() + }) +} + +async function startGrid() { + const port = await portprober.findFreePort() + const child = spawn('java', [ + '-jar', + SERVER_JAR, + 'standalone', + '--port', + String(port), + '--selenium-manager', + 'true', + '--enable-managed-downloads', + 'true', + '--log-level', + 'WARNING', + ]) + + const url = `http://localhost:${port}` + try { + await waitForServer(`${url}/status`, 60000) + } catch (err) { + child.kill() + throw err + } + + return { url, process: child } +} + +function stopGrid(child) { + if (child) { + child.kill() + } +} + +module.exports = { startGrid, stopGrid } diff --git a/examples/javascript/test/drivers/remote_webdriver.spec.js b/examples/javascript/test/drivers/remote_webdriver.spec.js new file mode 100644 index 000000000000..92f5de495282 --- /dev/null +++ b/examples/javascript/test/drivers/remote_webdriver.spec.js @@ -0,0 +1,96 @@ +const { Browser, Builder, By } = require('selenium-webdriver') +const Chrome = require('selenium-webdriver/chrome') +const remote = require('selenium-webdriver/remote') +const assert = require('node:assert') +const fs = require('node:fs') +const os = require('node:os') +const path = require('node:path') +const { startGrid, stopGrid } = require('./gridServer') + +async function waitForFile(filePath, timeout) { + const deadline = Date.now() + timeout + while (!fs.existsSync(filePath)) { + if (Date.now() > deadline) { + throw new Error(`Timed out waiting for ${filePath} to be written`) + } + await new Promise((resolve) => setTimeout(resolve, 100)) + } +} + +describe('Remote WebDriver Test', function () { + let driver + let gridUrl + let gridProcess + + beforeEach(async function () { + const grid = await startGrid() + gridUrl = grid.url + gridProcess = grid.process + }) + + afterEach(async function () { + try { + if (driver) { + await driver.quit() + driver = null + } + } finally { + stopGrid(gridProcess) + } + }) + + it('Basic Example', async function () { + const options = new Chrome.Options().addArguments('--no-sandbox') + driver = new Builder().forBrowser(Browser.CHROME).setChromeOptions(options).usingServer(gridUrl).build() + }) + + it('Uploads', async function () { + const options = new Chrome.Options().addArguments('--no-sandbox') + driver = new Builder().forBrowser(Browser.CHROME).setChromeOptions(options).usingServer(gridUrl).build() + await driver.get('https://the-internet.herokuapp.com/upload') + const uploadFile = path.resolve('./test/resources/selenium-snapshot.png') + + await driver.setFileDetector(new remote.FileDetector()) + await driver.findElement(By.css('input[type=file]')).sendKeys(uploadFile) + await driver.findElement(By.id('file-submit')).click() + + const fileName = await driver.findElement(By.id('uploaded-files')).getText() + assert.strictEqual(fileName, 'selenium-snapshot.png') + }) + + it('Downloads', async function () { + const options = new Chrome.Options().addArguments('--no-sandbox').enableDownloads() + driver = new Builder().forBrowser(Browser.CHROME).setChromeOptions(options).usingServer(gridUrl).build() + + const fileNames = ['file_1.txt', 'file_2.jpg'] + await driver.get('https://www.selenium.dev/selenium/web/downloads/download.html') + await driver.findElement(By.id('file-1')).click() + await driver.findElement(By.id('file-2')).click() + await driver.wait(async (d) => { + const downloaded = await d.getDownloadableFiles() + return fileNames.every((name) => downloaded.includes(name)) + }, 5000) + + const files = await driver.getDownloadableFiles() + assert.deepStrictEqual(files.sort(), fileNames.sort()) + + const targetDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'downloads-')) + const downloadableFile = files[0] + await driver.downloadFile(downloadableFile, targetDirectory) + + const downloadedFilePath = path.join(targetDirectory, downloadableFile) + await waitForFile(downloadedFilePath, 5000) + const fileContent = fs.readFileSync(downloadedFilePath, 'utf8') + assert.strictEqual(fileContent.trim(), 'Hello, World!') + + await driver.deleteDownloadableFiles() + assert.deepStrictEqual(await driver.getDownloadableFiles(), []) + }) + + it('Browser specific functionalities', async function () { + const options = new Chrome.Options().addArguments('--no-sandbox') + driver = new Builder().forBrowser(Browser.CHROME).setChromeOptions(options).usingServer(gridUrl).build() + + assert.ok(driver instanceof Chrome.Driver) + }) +}) diff --git a/website_and_docs/content/documentation/webdriver/drivers/remote_webdriver.en.md b/website_and_docs/content/documentation/webdriver/drivers/remote_webdriver.en.md index eed4543c0da5..56106e8ff66f 100644 --- a/website_and_docs/content/documentation/webdriver/drivers/remote_webdriver.en.md +++ b/website_and_docs/content/documentation/webdriver/drivers/remote_webdriver.en.md @@ -49,7 +49,7 @@ and an options instance are both required. {{< gh-codeblock path="/examples/ruby/spec/drivers/remote_webdriver_spec.rb#L20-L21" >}} {{< /tab >}} {{< tab header="JavaScript" >}} -{{< badge-code >}} +{{< gh-codeblock path="/examples/javascript/test/drivers/remote_webdriver.spec.js#L43-L44" >}} {{< /tab >}} {{< tab header="Kotlin" >}} {{< badge-code >}} @@ -85,7 +85,8 @@ Ruby adds a local file detector to remote webdriver instances by default, but yo {{< gh-codeblock path="/examples/ruby/spec/drivers/remote_webdriver_spec.rb#L33-L36" >}} {{< /tab >}} {{< tab header="JavaScript" >}} -{{< badge-code >}} +JavaScript does not include a Local File Detector by default, so you must always add one to do uploads. +{{< gh-codeblock path="/examples/javascript/test/drivers/remote_webdriver.spec.js#L53-L55" >}} {{< /tab >}} {{< tab header="Kotlin" >}} {{< badge-code >}} @@ -126,7 +127,7 @@ Each of the bindings have a method in the options class to set this. {{< gh-codeblock path="/examples/ruby/spec/drivers/remote_webdriver_spec.rb#L43-L44" >}} {{< /tab >}} {{< tab header="JavaScript" >}} -{{< badge-code >}} +{{< gh-codeblock path="/examples/javascript/test/drivers/remote_webdriver.spec.js#L62-L63" >}} {{< /tab >}} {{< tab header="Kotlin" >}} {{< badge-code >}} @@ -152,7 +153,7 @@ so the list is an immediate snapshot of what file names are currently in the dir {{< gh-codeblock path="/examples/ruby/spec/drivers/remote_webdriver_spec.rb#L52" >}} {{< /tab >}} {{< tab header="JavaScript" >}} -{{< badge-code >}} +{{< gh-codeblock path="/examples/javascript/test/drivers/remote_webdriver.spec.js#L74" >}} {{< /tab >}} {{< tab header="Kotlin" >}} {{< badge-code >}} @@ -177,7 +178,7 @@ Selenium looks for the name of the provided file in the list and downloads it to {{< gh-codeblock path="/examples/ruby/spec/drivers/remote_webdriver_spec.rb#L57" >}} {{< /tab >}} {{< tab header="JavaScript" >}} -{{< badge-code >}} +{{< gh-codeblock path="/examples/javascript/test/drivers/remote_webdriver.spec.js#L79" >}} {{< /tab >}} {{< tab header="Kotlin" >}} {{< badge-code >}} @@ -203,7 +204,7 @@ but you can also delete all files during the session. {{< gh-codeblock path="/examples/ruby/spec/drivers/remote_webdriver_spec.rb#L62" >}} {{< /tab >}} {{< tab header="JavaScript" >}} -{{< badge-code >}} +{{< gh-codeblock path="/examples/javascript/test/drivers/remote_webdriver.spec.js#L86" >}} {{< /tab >}} {{< tab header="Kotlin" >}} {{< badge-code >}} @@ -239,7 +240,9 @@ Ruby uses mixins to add applicable browser specific methods to the Remote WebDri the methods should always just work for you. {{< /tab >}} {{< tab header="JavaScript" >}} -{{< badge-code >}} +JavaScript returns the browser-specific driver class (such as `chrome.Driver`) directly when connecting to a +Remote Session, so no augmenting step is needed to access browser specific functionality. +{{< gh-codeblock path="/examples/javascript/test/drivers/remote_webdriver.spec.js#L91-L94" >}} {{< /tab >}} {{< tab header="Kotlin" >}} {{< badge-code >}}