Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

general: more consistency with promnesia project setup #64

Merged
merged 1 commit into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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