| // Copyright 2015 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import {assertEquals, assertNotEquals, assertTrue} from 'chrome://webui-test/chai_assert.js'; |
| |
| suite('WebviewContentScriptTest', function() { |
| const REQUEST_TO_COMM_CHANNEL_1 = 'connect'; |
| const REQUEST_TO_COMM_CHANNEL_2 = 'connect_request'; |
| const RESPONSE_FROM_COMM_CHANNEL_1 = 'connected'; |
| const RESPONSE_FROM_COMM_CHANNEL_2 = 'connected_response'; |
| |
| function createWebview(): chrome.webviewTag.WebView { |
| const webview = |
| document.createElement('webview') as chrome.webviewTag.WebView; |
| document.body.appendChild(webview); |
| return webview; |
| } |
| |
| function checkBackgroundColor(webview: chrome.webviewTag.WebView): |
| Promise<void> { |
| return new Promise<void>(resolve => { |
| webview.executeScript( |
| {code: 'document.body.style.backgroundColor;'}, (results: any[]) => { |
| assertEquals(1, results.length); |
| assertEquals('red', results[0]); |
| resolve(); |
| }); |
| }); |
| } |
| |
| function getWebviewUrl(): string { |
| return (window as unknown as Window & {webviewUrl: string}).webviewUrl; |
| } |
| |
| test('ExecuteScriptCode', async () => { |
| const webview = createWebview(); |
| |
| const whenLoaded = new Promise<void>(resolve => { |
| webview.addEventListener('loadstop', () => { |
| webview.executeScript( |
| {code: 'document.body.style.backgroundColor = \'red\';'}, |
| (_results: any[]) => { |
| resolve(); |
| }); |
| }); |
| }); |
| webview.src = getWebviewUrl(); |
| await whenLoaded; |
| await checkBackgroundColor(webview); |
| }); |
| |
| test('ExecuteScriptCodeFromFile', async () => { |
| const webview = createWebview(); |
| const whenLoaded = new Promise<void>(resolve => { |
| webview.addEventListener('loadstop', () => { |
| webview.executeScript({file: 'test/webview_execute_script.js'}, () => { |
| resolve(); |
| }); |
| }); |
| }); |
| webview.src = getWebviewUrl(); |
| await whenLoaded; |
| await checkBackgroundColor(webview); |
| }); |
| |
| // This test verifies that a content script will be injected to the webview |
| // when the webview is navigated to a page that matches the URL pattern |
| // defined in the content script. |
| test('AddContentScript', async () => { |
| const webview = |
| document.createElement('webview') as chrome.webviewTag.WebView; |
| |
| console.info('Step 1: call <webview>.addContentScripts.'); |
| webview.addContentScripts([{ |
| name: 'myrule', |
| matches: ['http://*/empty*'], |
| js: { |
| files: ['test/inject_comm_channel.js', 'test/inject_comm_channel_2.js'], |
| }, |
| run_at: 'document_start' as chrome.extensionTypes.RunAt, |
| }]); |
| |
| webview.addEventListener('loadstop', function() { |
| console.info('Step 2: postMessage to build connection.'); |
| const msg = [REQUEST_TO_COMM_CHANNEL_1]; |
| assertTrue(!!webview.contentWindow); |
| webview.contentWindow!.postMessage(JSON.stringify(msg), '*'); |
| }); |
| |
| const whenMessageReceived = new Promise<void>(resolve => { |
| window.addEventListener('message', function(e) { |
| if (e.source !== webview.contentWindow) { |
| return; |
| } |
| const data = JSON.parse(e.data); |
| assertEquals( |
| RESPONSE_FROM_COMM_CHANNEL_1, data[0], |
| 'Unexpected message: \'' + data[0] + '\''); |
| console.info( |
| 'Step 3: A communication channel has been established with ' + |
| 'webview.'); |
| resolve(); |
| }); |
| }); |
| |
| webview.src = getWebviewUrl(); |
| document.body.appendChild(webview); |
| await whenMessageReceived; |
| }); |
| |
| // Adds two content scripts with the same URL pattern to <webview> at the same |
| // time. This test verifies that both scripts are injected when the <webview> |
| // navigates to a URL that matches the URL pattern. |
| test('AddMultiContentScripts', async () => { |
| const webview = |
| document.createElement('webview') as chrome.webviewTag.WebView; |
| |
| console.info('Step 1: call <webview>.addContentScripts(myrule1 & myrule2)'); |
| webview.addContentScripts([ |
| { |
| name: 'myrule1', |
| matches: ['http://*/empty*'], |
| js: {files: ['test/inject_comm_channel.js']}, |
| run_at: 'document_start' as chrome.extensionTypes.RunAt, |
| }, |
| { |
| name: 'myrule2', |
| matches: ['http://*/empty*'], |
| js: {files: ['test/inject_comm_channel_2.js']}, |
| run_at: 'document_start' as chrome.extensionTypes.RunAt, |
| }, |
| ]); |
| |
| webview.addEventListener('loadstop', function() { |
| console.info('Step 2: postMessage to build connection.'); |
| const msg1 = [REQUEST_TO_COMM_CHANNEL_1]; |
| webview.contentWindow!.postMessage(JSON.stringify(msg1), '*'); |
| console.info( |
| 'Step 3: postMessage to build connection to the other script.'); |
| const msg2 = [REQUEST_TO_COMM_CHANNEL_2]; |
| webview.contentWindow!.postMessage(JSON.stringify(msg2), '*'); |
| }); |
| |
| let response1 = false; |
| let response2 = false; |
| const whenResponse = new Promise<void>(resolve => { |
| window.addEventListener('message', function(e) { |
| if (e.source !== webview.contentWindow) { |
| return; |
| } |
| const data = JSON.parse(e.data); |
| if (data[0] === RESPONSE_FROM_COMM_CHANNEL_1) { |
| console.info( |
| 'Step 4: A communication channel has been established with ' + |
| 'webview.'); |
| response1 = true; |
| if (response1 && response2) { |
| resolve(); |
| } |
| return; |
| } |
| assertEquals( |
| RESPONSE_FROM_COMM_CHANNEL_2, data[0], |
| 'Unexpected message: \'' + data[0] + '\''); |
| console.info( |
| 'Step 5: A communication channel has been established with ' + |
| 'webview.'); |
| response2 = true; |
| if (response1 && response2) { |
| resolve(); |
| } |
| return; |
| }); |
| }); |
| |
| webview.src = getWebviewUrl(); |
| document.body.appendChild(webview); |
| await whenResponse; |
| }); |
| |
| // Adds a content script to <webview> and navigates. After seeing the script |
| // is injected, we add another content script with the same name to the |
| // <webview>. This test verifies that the second script will replace the first |
| // one and be injected after navigating the <webview>. Meanwhile, the |
| // <webview> shouldn't get any message from the first script anymore. |
| test( |
| 'AddContentScriptWithSameNameShouldOverwriteTheExistingOne', async () => { |
| const webview = |
| document.createElement('webview') as chrome.webviewTag.WebView; |
| |
| console.info('Step 1: call <webview>.addContentScripts(myrule1)'); |
| webview.addContentScripts([{ |
| name: 'myrule1', |
| matches: ['http://*/empty*'], |
| js: {files: ['test/inject_comm_channel.js']}, |
| run_at: 'document_start' as chrome.extensionTypes.RunAt, |
| }]); |
| let connectScript1 = true; |
| let connectScript2 = false; |
| |
| webview.addEventListener('loadstop', function() { |
| if (connectScript1) { |
| const msg1 = [REQUEST_TO_COMM_CHANNEL_1]; |
| webview.contentWindow!.postMessage(JSON.stringify(msg1), '*'); |
| connectScript1 = false; |
| } |
| if (connectScript2) { |
| const msg2 = [REQUEST_TO_COMM_CHANNEL_2]; |
| webview.contentWindow!.postMessage(JSON.stringify(msg2), '*'); |
| connectScript2 = false; |
| } |
| }); |
| |
| let shouldGetResponseFromScript1 = true; |
| const whenChannel2ResponseReceived = new Promise<void>(resolve => { |
| window.addEventListener('message', function(e) { |
| if (e.source !== webview.contentWindow) { |
| return; |
| } |
| const data = JSON.parse(e.data); |
| if (data[0] === RESPONSE_FROM_COMM_CHANNEL_1) { |
| assertTrue(shouldGetResponseFromScript1); |
| console.info( |
| 'Step 2: A communication channel has been established with ' + |
| 'webview.'); |
| console.info( |
| 'Step 3: <webview>.addContentScripts() with a updated' + |
| ' \'myrule1\''); |
| webview.addContentScripts([{ |
| name: 'myrule1', |
| matches: ['http://*/empty*'], |
| js: {files: ['test/inject_comm_channel_2.js']}, |
| run_at: 'document_start' as chrome.extensionTypes.RunAt, |
| }]); |
| connectScript2 = true; |
| shouldGetResponseFromScript1 = false; |
| webview.src = getWebviewUrl(); |
| return; |
| } |
| assertEquals( |
| RESPONSE_FROM_COMM_CHANNEL_2, data[0], |
| 'Unexpected message : \'' + data[0] + '\''); |
| console.info( |
| 'Step 4: Another communication channel has been established ' + |
| 'with webview.'); |
| resolve(); |
| }); |
| }); |
| |
| webview.src = getWebviewUrl(); |
| document.body.appendChild(webview); |
| await whenChannel2ResponseReceived; |
| }); |
| |
| // There are two <webview>s are added to the DOM, and we add a content script |
| // to one of them. This test verifies that the script won't be injected in |
| // the other <webview>. |
| test( |
| 'AddContentScriptToOneWebViewShouldNotInjectToTheOtherWebView', |
| async () => { |
| const webview1 = |
| document.createElement('webview') as chrome.webviewTag.WebView; |
| const webview2 = |
| document.createElement('webview') as chrome.webviewTag.WebView; |
| |
| console.info('Step 1: call <webview1>.addContentScripts.'); |
| webview1.addContentScripts([{ |
| name: 'myrule', |
| matches: ['http://*/empty*'], |
| js: {files: ['test/inject_comm_channel.js']}, |
| run_at: 'document_start' as chrome.extensionTypes.RunAt, |
| }]); |
| |
| const whenMessagePosted = new Promise<void>(resolve => { |
| webview2.addEventListener('loadstop', function() { |
| console.info( |
| 'Step 2: webview2 requests to build communication channel.'); |
| const msg = [REQUEST_TO_COMM_CHANNEL_1]; |
| webview2.contentWindow!.postMessage(JSON.stringify(msg), '*'); |
| setTimeout(function() { |
| resolve(); |
| }, 0); |
| }); |
| }); |
| |
| window.addEventListener('message', function(e) { |
| assertNotEquals( |
| webview2.contentWindow, e.source, |
| 'Unexpected message : \'' + JSON.parse(e.data)[0] + '\''); |
| }); |
| webview1.src = getWebviewUrl(); |
| webview2.src = getWebviewUrl(); |
| document.body.appendChild(webview1); |
| document.body.appendChild(webview2); |
| await whenMessagePosted; |
| }); |
| |
| // Adds a content script to <webview> and navigates to a URL that matches the |
| // URL pattern defined in the script. After the first navigation, we remove |
| // this script from the <webview> and navigates to the same URL. This test |
| // verifies that the script is injected during the first navigation, but isn't |
| // injected after removing it. |
| test('AddAndRemoveContentScripts', async () => { |
| const webview = |
| document.createElement('webview') as chrome.webviewTag.WebView; |
| |
| console.info('Step 1: call <webview>.addContentScripts.'); |
| webview.addContentScripts([{ |
| name: 'myrule', |
| matches: ['http://*/empty*'], |
| js: {files: ['test/inject_comm_channel.js']}, |
| run_at: 'document_start' as chrome.extensionTypes.RunAt, |
| }]); |
| |
| let shouldGetResponseFromScript1 = true; |
| |
| let count = 0; |
| const whenPostSecondMessage = new Promise<void>(resolve => { |
| webview.addEventListener('loadstop', function() { |
| if (count === 0) { |
| console.info('Step 2: post message to build connect.'); |
| const msg = [REQUEST_TO_COMM_CHANNEL_1]; |
| webview.contentWindow!.postMessage(JSON.stringify(msg), '*'); |
| ++count; |
| } else if (count === 1) { |
| console.info('Step 5: post message to build connect again.'); |
| const msg = [REQUEST_TO_COMM_CHANNEL_1]; |
| webview.contentWindow!.postMessage(JSON.stringify(msg), '*'); |
| setTimeout(function() { |
| resolve(); |
| }, 0); |
| } |
| }); |
| }); |
| |
| window.addEventListener('message', function(e) { |
| if (e.source !== webview.contentWindow) { |
| return; |
| } |
| const data = JSON.parse(e.data); |
| assertTrue(shouldGetResponseFromScript1); |
| assertEquals( |
| RESPONSE_FROM_COMM_CHANNEL_1, data[0], |
| 'Unexpected message: \'' + data[0] + '\''); |
| console.info( |
| 'Step 3: A communication channel has been established ' + |
| 'with webview.'); |
| shouldGetResponseFromScript1 = false; |
| console.info('Step 4: call <webview>.removeContentScripts and navigate.'); |
| webview.removeContentScripts(); |
| webview.src = getWebviewUrl(); |
| }); |
| |
| webview.src = getWebviewUrl(); |
| document.body.appendChild(webview); |
| await whenPostSecondMessage; |
| }); |
| |
| // This test verifies that the addContentScripts API works with the new window |
| // API. |
| test('AddContentScriptsWithNewWindowAPI', async () => { |
| const webview = |
| document.createElement('webview') as chrome.webviewTag.WebView; |
| |
| let newwebview: chrome.webviewTag.WebView; |
| webview.addEventListener('newwindow', function(e) { |
| e.preventDefault(); |
| newwebview = |
| document.createElement('webview') as chrome.webviewTag.WebView; |
| |
| console.info('Step 2: call newwebview.addContentScripts.'); |
| newwebview.addContentScripts([{ |
| name: 'myrule', |
| matches: ['http://*/guest_from_opener*'], |
| js: {files: ['test/inject_comm_channel.js']}, |
| run_at: 'document_start' as chrome.extensionTypes.RunAt, |
| }]); |
| |
| newwebview.addEventListener('loadstop', function() { |
| const msg = [REQUEST_TO_COMM_CHANNEL_1]; |
| console.info( |
| 'Step 4: new webview postmessage to build communication ' + |
| 'channel.'); |
| newwebview.contentWindow!.postMessage(JSON.stringify(msg), '*'); |
| }); |
| |
| document.body.appendChild(newwebview); |
| // attach the new window to the new <webview>. |
| console.info('Step 3: attaches the new webview.'); |
| const newwindow = (e as Event & {window: Window}).window; |
| (newwindow as Window & { |
| attach: (frame: HTMLIFrameElement) => void, |
| }).attach(newwebview); |
| }); |
| |
| const whenResponseFromChannel1 = new Promise<void>(resolve => { |
| window.addEventListener('message', function(e) { |
| if (!newwebview || e.source !== newwebview.contentWindow) { |
| return; |
| } |
| const data = JSON.parse(e.data); |
| assertEquals(newwebview.contentWindow, e.source); |
| assertEquals( |
| RESPONSE_FROM_COMM_CHANNEL_1, data[0], |
| 'Unexpected message: \'' + data[0] + '\''); |
| console.info( |
| 'Step 5: a communication channel has been established ' + |
| 'with the new webview.'); |
| resolve(); |
| }); |
| }); |
| |
| console.info('Step 1: navigates the webview to window open guest URL.'); |
| webview.setAttribute('src', getWebviewUrl()); |
| document.body.appendChild(webview); |
| await whenResponseFromChannel1; |
| }); |
| |
| // Adds a content script to <webview>. This test verifies that the script is |
| // injected after terminate and reload <webview>. |
| test('ContentScriptIsInjectedAfterTerminateAndReloadWebView', async () => { |
| const webview = |
| document.createElement('webview') as chrome.webviewTag.WebView; |
| |
| console.info('Step 1: call <webview>.addContentScripts.'); |
| webview.addContentScripts([{ |
| name: 'myrule', |
| matches: ['http://*/empty*'], |
| js: {files: ['test/webview_execute_script.js']}, |
| run_at: 'document_end' as chrome.extensionTypes.RunAt, |
| }]); |
| |
| let count = 0; |
| const onGetBackgroundExecuted = new Promise<void>(resolve => { |
| webview.addEventListener('loadstop', function() { |
| if (count === 0) { |
| console.info('Step 2: call webview.terminate().'); |
| webview.terminate(); |
| ++count; |
| return; |
| } else if (count === 1) { |
| console.info('Step 4: call <webview>.executeScript to check result.'); |
| checkBackgroundColor(webview).then(resolve); |
| } |
| }); |
| }); |
| |
| webview.addEventListener('exit', function() { |
| console.info('Step 3: call webview.reload().'); |
| webview.reload(); |
| }); |
| |
| webview.src = getWebviewUrl(); |
| document.body.appendChild(webview); |
| await onGetBackgroundExecuted; |
| }); |
| |
| // This test verifies the content script won't be removed when the guest is |
| // destroyed, i.e., removed <webview> from the DOM. |
| test('ContentScriptExistsAsLongAsWebViewTagExists', async () => { |
| const webview = |
| document.createElement('webview') as chrome.webviewTag.WebView; |
| |
| console.info('Step 1: call <webview>.addContentScripts.'); |
| webview.addContentScripts([{ |
| name: 'myrule', |
| matches: ['http://*/empty*'], |
| js: {files: ['test/webview_execute_script.js']}, |
| run_at: 'document_end' as chrome.extensionTypes.RunAt, |
| }]); |
| |
| let count = 0; |
| const whenBackgroundColorChecked = new Promise<void>(resolve => { |
| webview.addEventListener('loadstop', function() { |
| if (count === 0) { |
| console.info('Step 2: check the result of content script injected.'); |
| webview.executeScript( |
| {code: 'document.body.style.backgroundColor;'}, |
| function(results) { |
| assertEquals(1, results.length); |
| assertEquals('red', results[0]); |
| |
| console.info('Step 3: remove webview from the DOM.'); |
| document.body.removeChild(webview); |
| console.info('Step 4: add webview back to the DOM.'); |
| document.body.appendChild(webview); |
| ++count; |
| }); |
| } else if (count === 1) { |
| console.info( |
| 'Step 5: check the result of content script injected again.'); |
| checkBackgroundColor(webview).then(resolve); |
| } |
| }); |
| }); |
| |
| webview.src = getWebviewUrl(); |
| document.body.appendChild(webview); |
| await whenBackgroundColorChecked; |
| }); |
| |
| test('AddContentScriptWithCode', async () => { |
| const webview = |
| document.createElement('webview') as chrome.webviewTag.WebView; |
| |
| console.info('Step 1: call <webview>.addContentScripts.'); |
| webview.addContentScripts([{ |
| name: 'myrule', |
| matches: ['http://*/empty*'], |
| js: {code: 'document.body.style.backgroundColor = \'red\';'}, |
| run_at: 'document_end' as chrome.extensionTypes.RunAt, |
| }]); |
| |
| const whenLoadStop = new Promise<void>(resolve => { |
| webview.addEventListener('loadstop', function() { |
| console.info('Step 2: call webview.executeScript() to check result.'); |
| resolve(); |
| }); |
| }); |
| |
| webview.src = getWebviewUrl(); |
| document.body.appendChild(webview); |
| await whenLoadStop; |
| await checkBackgroundColor(webview); |
| }); |
| }); |