Skip to content

Commit

Permalink
Wip tab api
Browse files Browse the repository at this point in the history
  • Loading branch information
trickypr committed Mar 17, 2024
1 parent 28da8a4 commit 4f802fa
Show file tree
Hide file tree
Showing 15 changed files with 336 additions and 154 deletions.
21 changes: 13 additions & 8 deletions apps/content/src/browser/lib/window/tab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ let localTabId = 0
* This provides a consistent internal representation of a tab, including the
* browser elements it contains & information derived from listeners about its current state
*/
export class Tab {
export class Tab implements ITab {
private _id: number = ++localTabId
private tabId: number | undefined

Expand All @@ -31,7 +31,7 @@ export class Tab {

// Publicly available data. Even though these are writable, updating them will not change
// the state of the browser element
public title = writable('')
public title = viewableWritable('')
public icon: ViewableWritable<string | null> = viewableWritable(null)
public uri: ViewableWritable<nsIURIType>
public bookmarkInfo: Writable<BookmarkTreeNode | null> = writable(null)
Expand All @@ -48,7 +48,7 @@ export class Tab {
public zoom = writable(1)

public focusedOmnibox = writable(true)
public hidden = writable(false)
public hidden = viewableWritable(false)

constructor(uri: nsIURIType) {
this.browserElement = createBrowser({
Expand Down Expand Up @@ -94,6 +94,10 @@ export class Tab {
return this.tabId || 0
}

public getWindowId(): number {
return window.windowApi.id
}

public getBrowserElement() {
return this.browserElement
}
Expand Down Expand Up @@ -241,24 +245,25 @@ export class Tab {
this.showFindBar()
}

public swapWithTab(tab: Tab) {
public swapWithTab(tab: ITab) {
this.removeEventListeners()
tab.removeEventListeners()

this.browserElement.swapDocShells(tab.browserElement)
this.browserElement.swapDocShells(tab.getBrowserElement())

this.useEventListeners()
tab.useEventListeners()

if (this.browserElement.id) this.tabId = this.browserElement.browserId
if (tab.browserElement.id) tab.tabId = tab.browserElement.browserId
if (tab.getBrowserElement().id)
tab.tabId = tab.getBrowserElement().browserId

const otherTitle = get(tab.title)
const otherTitle = tab.title.readOnce()
const otherIcon = get(tab.icon)
const otherUri = get(tab.uri)
const otherBookmarkInfo = get(tab.bookmarkInfo)

tab.title.set(get(this.title))
tab.title.set(this.title.readOnce())
tab.icon.set(get(this.icon))
tab.uri.set(get(this.uri))
tab.bookmarkInfo.set(get(this.bookmarkInfo))
Expand Down
1 change: 1 addition & 0 deletions apps/extensions/lib/ext-browser.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"paths": [["pageAction"]]
},
"tabs": {
"url": "chrome://bextensions/content/parent/ext-tabs.js",
"schema": "chrome://bextensions/content/schemas/tabs.json",
"scopes": ["addon_parent"],
"paths": [["tabs"]]
Expand Down
120 changes: 119 additions & 1 deletion apps/extensions/lib/parent/ext-tabs.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,127 @@
// @ts-check
/// <reference path="../types/index.d.ts" />
/// <reference path="./ext-browser.js" />

const { serialize } = require('v8')

/**
* @typedef {'capture' | 'extension' | 'user'} MuteInfoReason
*
* @typedef {'loading' | 'complete'} TabStatus
*
* @typedef {object} MutedInfo
* @property {string} [extensionId]
* @property {boolean} muted
* @property {MuteInfoReason} reason
*
* @typedef {object} SharingState
* @property {'screen' | 'window' | 'application'} [screen]
* @property {boolean} camera
* @property {boolean} microphone
*
* @typedef {object} ExtTab
* @property {boolean} active
* @property {boolean} [attention]
* @property {boolean} [audible]
* @property {boolean} [autoDiscardable]
* @property {string} [cookieStoreId]
* @property {boolean} [discarded]
* @property {string} [favIconUrl]
* @property {number} [height]
* @property {boolean} hidden
* @property {boolean} highlighted
* @property {number} [id]
* @property {boolean} incognito
* @property {number} index
* @property {boolean} isArticle
* @property {number} [lastAccessed]
* @property {MutedInfo} [mutedInfo]
* @property {number} [openerTabId]
* @property {boolean} pinned
* @property {string} sessionId
* @property {TabStatus} [status]
* @property {number} [successorTabId]
* @property {string} [title]
* @property {string} [url]
* @property {number} [width]
* @property {number} windowId
*
* @typedef {object} queryInfo
* @property {boolean} [active]
* @property {boolean} [attention]
* @property {boolean} [pinned]
* @property {boolean} [audible]
* @property {boolean} [autoDiscardable]
* @property {boolean} [muted]
* @property {boolean} [highlighted]
* @property {boolean} [currentWindow]
* @property {boolean} [lastFocusedWindow]
* @property {TabStatus} [status]
* @property {boolean} [discarded]
* @property {boolean} [hidden]
* @property {string} [title]
* @property {string | string[]} [url]
* @property {number} [windowId]
* @property {WindowType} [windowType]
* @property {number} [index]
* @property {string | string[]} [cookieStoreId]
*/

/**
* @param {queryInfo} queryInfo
*/
function query(queryInfo) {
const windows = [...lazy.WindowTracker.registeredWindows.entries()]

const urlMatchSet =
(queryInfo.url &&
(Array.isArray(queryInfo.url)
? new MatchPatternSet(queryInfo.url)
: new MatchPatternSet([queryInfo.url]))) ||
null

return windows.flatMap(([id, window]) =>
window.windowApi.tabs.tabs.filter((tab) => {
const uri =
urlMatchSet === null ? true : urlMatchSet.matches(tab.uri.readOnce())
const windowId = queryInfo.windowId ? id === queryInfo.windowId : true

return uri && windowId
}),
)
}

/**
* @param {ITab} tab
* @returns {ExtTab}
*/
const serizlise = (tab) => ({
active: true,
hidden: tab.hidden.readOnce(),
highlighted: false,
incognito: false,
index: -1, // TODO:
isArticle: false,
pinned: false,
sessionId: '',
windowId: tab.getWindowId(),
})

this.tabs = class extends ExtensionAPIPersistent {
/**
* @param {BaseContext} context
*/
getAPI(context) {}
getAPI(context) {
return {
tabs: {
/**
* @param {queryInfo} queryInfo
*/
async query(queryInfo) {
console.log(queryInfo)
return query(queryInfo).map(serialize)
},
},
}
}
}
32 changes: 26 additions & 6 deletions apps/modules/lib/ExtensionTestUtils.sys.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -174,10 +174,8 @@ const objectMap = (obj, fn) =>
*/
class ExtensionTestUtilsImpl {
/**
* @template {import('resource://app/modules/zora.sys.mjs').IAssert} A
*
* @param {Partial<import("resource://app/modules/ExtensionTestUtils.sys.mjs").ExtManifest>} definition
* @param {import('resource://app/modules/ExtensionTestUtils.sys.mjs').AddonMiddleware<A>} assert
* @param {import('resource://app/modules/TestManager.sys.mjs').IDefaultAssert} assert
*
* @returns {import('resource://app/modules/ExtensionTestUtils.sys.mjs').ExtensionWrapper}
*/
Expand All @@ -189,8 +187,13 @@ class ExtensionTestUtilsImpl {
definition.background && serializeScript(definition.background),
})

let testCount = 0
/** @type {number | null} */
let expectedTestCount = null

function handleTestResults(kind, pass, msg, ...args) {
if (kind == 'test-eq') {
testCount += 1
let [expected, actual] = args
assert.ok(pass, `${msg} - Expected: ${expected}, Actual: ${actual}`)
} else if (kind == 'test-log') {
Expand Down Expand Up @@ -228,25 +231,42 @@ class ExtensionTestUtilsImpl {
/* Ignore */
}
await extension.startup()
return await startupPromise
await startupPromise
} catch (e) {
assert.fail(`Errored: ${e}`)
}

return this
},
async unload() {
await extension.shutdown()
return await extension._uninstallPromise
await extension._uninstallPromise

if (expectedTestCount && testCount !== expectedTestCount) {
assert.fail(
`Expected ${expectedTestCount} to execute. ${testCount} extecuted instead`,
)
}
},

/**
* @param {number} count
*/
testCount(count) {
expectedTestCount = count
return this
},
sendMsg(msg) {
extension.testMessage(msg)
return this
},
async awaitMsg(msg) {
const self = this
return new Promise((res) => {
const callback = (_, event) => {
if (event == msg) {
extension.off('test-message', callback)
res(void 0)
res(self)
}
}

Expand Down
1 change: 1 addition & 0 deletions apps/tests/integrations/_index.sys.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
// @ts-check
/// <reference types="@browser/link" />
import './extensions/pageAction.mjs'
import './extensions/tabs.mjs'
19 changes: 2 additions & 17 deletions apps/tests/integrations/extensions/pageAction.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ async function spinLock(predicate) {
}

await TestManager.withBrowser('http://example.com/', async (window) => {
await TestManager.test('Extension Test', async (test) => {
await TestManager.test('pageAction', async (test) => {
const extension = ExtensionTestUtils.loadExtension(
{
manifest: {
Expand All @@ -27,19 +27,6 @@ await TestManager.withBrowser('http://example.com/', async (window) => {
show_matches: ['<all_urls>'],
},
},
async background() {
const { browser } = this

browser.test.assertTrue(true, 'True is true')
browser.test.assertEq(1, 1, 'EQ')
browser.test.log('log')
browser.test.sendMessage('msg')
browser.test.succeed('succeed')

browser.test.onMessage.addListener((msg) =>
setTimeout(() => browser.test.sendMessage(`${msg}:done`), 100),
)
},
files: {
'flask-line.svg': `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M15.9994 2V4H14.9994V7.24291C14.9994 8.40051 15.2506 9.54432 15.7357 10.5954L20.017 19.8714C20.3641 20.6236 20.0358 21.5148 19.2836 21.8619C19.0865 21.9529 18.8721 22 18.655 22H5.34375C4.51532 22 3.84375 21.3284 3.84375 20.5C3.84375 20.2829 3.89085 20.0685 3.98181 19.8714L8.26306 10.5954C8.74816 9.54432 8.99939 8.40051 8.99939 7.24291V4H7.99939V2H15.9994ZM13.3873 10.0012H10.6115C10.5072 10.3644 10.3823 10.7221 10.2371 11.0724L10.079 11.4335L6.12439 20H17.8734L13.9198 11.4335C13.7054 10.9691 13.5276 10.4902 13.3873 10.0012ZM10.9994 7.24291C10.9994 7.49626 10.9898 7.7491 10.9706 8.00087H13.0282C13.0189 7.87982 13.0119 7.75852 13.0072 7.63704L12.9994 7.24291V4H10.9994V7.24291Z"></path></svg>`,
'pageaction.html': `
Expand All @@ -60,9 +47,7 @@ await TestManager.withBrowser('http://example.com/', async (window) => {
)

await extension.startup()

extension.sendMsg('test')
await extension.awaitMsg('test:done')
await new Promise((res) => queueMicrotask(res))

const pageActionId = `page-action-icon--${extension.extension.id}`
.replace('@', '')
Expand Down
35 changes: 35 additions & 0 deletions apps/tests/integrations/extensions/tabs.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// @ts-check
/// <reference types="@browser/link" />
import { ExtensionTestUtils } from 'resource://app/modules/ExtensionTestUtils.sys.mjs'
import { TestManager } from 'resource://app/modules/TestManager.sys.mjs'

await TestManager.withBrowser('http://example.com', async (window) => {
await TestManager.test('tabs', async (test) => {
const extension = ExtensionTestUtils.loadExtension(
{
manifest: { permissions: ['tabs'] },
async background() {
const { browser } = this

const exampleTabs = await browser.tabs.query({
url: 'http://example.com/',
})
browser.test.assertEq(
exampleTabs.length,
1,
'There must be at one tab matching `http://example.com/`',
)
},
},
test,
)

await extension
.testCount(1)
.startup()
.then((e) => e.unload())
})
})
1 change: 1 addition & 0 deletions libs/link/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
},
"devDependencies": {
"gecko-types": "github:quark-platform/gecko-types",
"svelte": "^4.2.8",
"zora": "^5.2.0"
}
}
Loading

0 comments on commit 4f802fa

Please sign in to comment.