Skip to content

Commit

Permalink
general: more consistency with promnesia project setup
Browse files Browse the repository at this point in the history
- update npm packages and enable jest support
- use end2end addon testing helpers from promnesia
  • Loading branch information
karlicoss committed May 23, 2024
1 parent c801b89 commit 58f2865
Show file tree
Hide file tree
Showing 11 changed files with 411 additions and 186 deletions.
3 changes: 1 addition & 2 deletions extension/.ci/build
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ npm install
FAILED=0

npm run eslint || FAILED=1
# TODO later
#npm run test || FAILED=1
npm run test || FAILED=1

for browser in 'firefox' 'chrome'; do
./build --target "$browser" "$@"
Expand Down
9 changes: 8 additions & 1 deletion extension/babel.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
const presets = [
'@babel/preset-flow',
// this is necessary for jest? otherwsie it can't import modules..
// ugh... I don't understand tbh, seems that even without preset-env, webpack respects browserlist??
// and looks like without preset-env the code is cleaner???
// but whatever, the difference is minor and I don't have energy to investigate now..
'@babel/preset-env',

// also necessary for jest? otherwise fails to import typescript
'@babel/preset-typescript',
]
const plugins = []

Expand Down
27 changes: 16 additions & 11 deletions extension/build
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import argparse
import os
from subprocess import check_call
from pathlib import Path
from sys import platform

# right, specifying id in manifest doesn't seem to work
# AMO responds with: Server response: Duplicate add-on ID found. (status: 400)
Expand All @@ -17,12 +18,14 @@ TARGETS = [
'firefox',
]

def main():
npm = "npm.cmd" if platform == "win32" else "npm"

def main() -> None:
p = argparse.ArgumentParser()
p.add_argument('--release', action='store_true', help="Use release flavor of build")
p.add_argument('--watch' , action='store_true')
p.add_argument('--lint' , action='store_true')
p.add_argument('--publish', action='store_true', help="Publish on chrome web store/addons.mozilla.org")
p.add_argument('--publish', choices=['listed', 'unlisted'], help="Publish on chrome web store/addons.mozilla.org")
p.add_argument('--v3', action='store_const', const='3', dest='manifest')
p.add_argument('--v2', action='store_const', const='2', dest='manifest')

Expand All @@ -41,9 +44,9 @@ def main():
ext_dir = (base_ext_dir / target).resolve() # webext can't into symlinks
# sadly no way to specify zip name in the regex..
artifacts_dir = (base_ext_dir / 'artifacts' / target).resolve()
def webext(*args, **kwargs):
def webext(*args, **kwargs) -> None:
check_call([
'npm', 'run', 'web-ext',
npm, 'run', 'web-ext',
'--',
'--source-dir' , ext_dir,
'--artifacts-dir', artifacts_dir,
Expand All @@ -53,21 +56,21 @@ def main():
env = {
'TARGET' : target,
'RELEASE': 'YES' if args.release else 'NO',
'PUBLISH': 'YES' if args.publish else 'NO',
'PUBLISH': 'YES' if args.publish is not None else 'NO',
'MANIFEST': manifest,
'EXT_ID' : IDS[target],
**os.environ,
}

if args.watch:
check_call([
'npm', 'run', 'watch',
npm, 'run', 'watch',
], env=env, cwd=str(Path(__file__).absolute().parent))
# TODO exec instead?
return

check_call([
'npm', 'run', 'build',
npm, 'run', 'build',
], env=env, cwd=str(Path(__file__).absolute().parent))

if args.lint:
Expand All @@ -91,20 +94,22 @@ def main():
'--id' , IDS[target],
]

if args.publish:
if args.publish is not None:
assert args.lint
assert args.release
if 'firefox' in target:
check_call([
'npm', 'run', 'release:amo',
npm, 'run', 'release:amo',
'--',
'--channel', 'listed',
'--channel', args.publish,
'--source-dir', str(ext_dir),
*firefox_release_args(),
])
elif target == 'chrome':
assert args.publish == 'listed' # no notion of unlisted on chrome store?
from chrome_dev_secrets import CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN
check_call([
'npm', 'run', 'release:cws',
npm, 'run', 'release:cws',
'--',
'upload',
# '--auto-publish',
Expand Down
8 changes: 8 additions & 0 deletions extension/eslint.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// @ts-check

const globals = require('globals')
const eslint = require('@eslint/js')
const tseslint = require('typescript-eslint')

Expand All @@ -20,5 +21,12 @@ module.exports = tseslint.config(
},
],
},
languageOptions: {
globals: {
// necessary for document. window. etc variables to work
...globals.browser,
...globals.webextensions,
},
},
},
)
20 changes: 13 additions & 7 deletions extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,23 +28,29 @@
},
"homepage": "https://github.com/karlicoss/grasp#readme",
"devDependencies": {
"@babel/core": "^7.24.4",
"@babel/eslint-parser": "^7.24.1",
"@babel/preset-env": "^7.24.4",
"@eslint/js": "^9.1.1",
"@babel/core": "^7.24.5",
"@babel/eslint-parser": "^7.24.5",
"@babel/preset-env": "^7.24.5",
"@babel/preset-typescript": "^7.24.1",
"@eslint/js": "^9.3.0",
"@types/webextension-polyfill": "^0.10.7",
"babel-loader": "^9.1.3",
"chrome-webstore-upload-cli": "^3.1.0",
"clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^12.0.2",
"css-loader": "^7.1.1",
"css-loader": "^7.1.2",
"eslint": "^8.57.0",
"globals": "^15.3.0",
"jest": "^29.5.0",
"jest-environment-jsdom": "^29.5.0",
"jest-fetch-mock": "^3.0.3",
"node-fetch": "^3.3.2",
"style-loader": "^4.0.0",
"ts-loader": "^9.5.1",
"typescript": "^5.4.5",
"typescript-eslint": "^7.7.1",
"typescript-eslint": "^7.10.0",
"web-ext": "^7.11.0",
"webextension-polyfill": "^0.11.0",
"webextension-polyfill": "^0.12.0",
"webpack": "^5.91.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.0.4",
Expand Down
4 changes: 4 additions & 0 deletions extension/tests/dummy.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
test('dummy', () => {
const hello = 'hello'
expect(hello).toBe('hello')
})
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ testing = [
]


[project.scripts]
grasp_backend = 'grasp_backend.__main__:main'


[build-system]
requires = ["setuptools", "setuptools-scm"]
build-backend = "setuptools.build_meta"
Expand Down
126 changes: 126 additions & 0 deletions tests/addon.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
"""
Grasp-specific addon wrappers
"""
from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
import time
from typing import Iterator

import click
import pytest
from selenium.webdriver import Remote as Driver

from .addon_helper import AddonHelper


def get_addon_source(kind: str) -> Path:
# TODO compile first?
addon_path = (Path(__file__).parent.parent / 'extension' / 'dist' / kind).absolute()
assert addon_path.exists()
assert (addon_path / 'manifest.json').exists()
return addon_path


class Command:
# TODO assert these against manifest?
CAPTURE_SIMPLE = 'capture-simple'
CAPTURE_EXTRA = '_execute_browser_action'
CAPTURE_EXTRA_V3 = '_execute_action'


@dataclass
class OptionsPage:
# I suppose it's inevitable it's at least somewhat driver aware? since we want it to locate elements etc
helper: AddonHelper

def open(self) -> None:
self.helper.open_page(self.helper.options_page_name)

def change_endpoint(self, endpoint: str, *, wait_for_permissions: bool = False) -> None:
driver = self.helper.driver

current_url = driver.current_url
assert current_url.endswith(self.helper.options_page_name), current_url # just in case

ep = driver.find_element('id', 'endpoint_id')
while ep.get_attribute('value') == '':
# data is set asynchronously, so need to wait for data to appear
# TODO is there some webdriver wait?
time.sleep(0.001)
ep.clear()
ep.send_keys(endpoint)

se = driver.find_element('id', 'save_id')
se.click()

if wait_for_permissions:
# we can't accept this alert via webdriver, it's a native chrome alert, not DOM
click.confirm(click.style('You should see prompt for permissions. Accept them', blink=True, fg='yellow'), abort=True)

alert = driver.switch_to.alert
assert alert.text == 'Saved!', alert.text # just in case
alert.accept()


@dataclass
class Popup:
addon: 'Addon'

def open(self) -> None:
self.addon.activate()
# time.sleep(2) # TODO not sure if can do better?

def enter_data(self, *, comment: str, tags: str) -> None:
helper = self.addon.helper

if helper.driver.name == 'firefox':
# for some reason in firefox under geckodriver it woudn't focus comment input field??
# tried both regular and dev edition firefox with latest geckodriver
# works fine when extension is loaded in firefox manually or in chrome with chromedriver..
# TODO file a bug??
helper.gui_hotkey('tab') # give focus to the input

helper.gui_write(comment)

helper.gui_hotkey('tab') # switch to tags

# erase default, without interval doesn't remove everything
for _ in range(10):
helper.gui_hotkey('backspace')
# pyautogui.hotkey(['backspace' for i in range(10)], interval=0.05)
helper.gui_write(tags)

def submit(self) -> None:
self.addon.helper.gui_hotkey('shift+enter')


@dataclass
class Addon:
helper: AddonHelper

def activate(self) -> None:
cmd = {
2: Command.CAPTURE_EXTRA,
3: Command.CAPTURE_EXTRA_V3, # meh
}[self.helper.manifest_version]
self.helper.trigger_command(cmd)

def quick_capture(self) -> None:
self.helper.trigger_command(Command.CAPTURE_SIMPLE)

@property
def options_page(self) -> OptionsPage:
return OptionsPage(helper=self.helper)

@property
def popup(self) -> Popup:
return Popup(addon=self)


@pytest.fixture
def addon(driver: Driver) -> Iterator[Addon]:
addon_source = get_addon_source(kind=driver.name)
helper = AddonHelper(driver=driver, addon_source=addon_source)
yield Addon(helper=helper)
Loading

0 comments on commit 58f2865

Please sign in to comment.