Skip to content

Commit 0454d7d

Browse files
[Google password manager] Android isolated module for password import (#1157)
* feat: add logic for password import * feat: add logic for password import * chore: add some comments * chore: PR comments * refactor: rename and update config structure * chore: pr comments * chore: remove file --------- Co-authored-by: Jonathan Kingston <[email protected]>
1 parent a44aaef commit 0454d7d

File tree

11 files changed

+559
-5
lines changed

11 files changed

+559
-5
lines changed
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { test } from '@playwright/test'
2+
import { readFileSync } from 'fs'
3+
import {
4+
mockAndroidMessaging,
5+
wrapWebkitScripts
6+
} from '@duckduckgo/messaging/lib/test-utils.mjs'
7+
import { perPlatform } from './type-helpers.mjs'
8+
9+
test('Password import feature', async ({ page }, testInfo) => {
10+
const passwordImportFeature = AutofillPasswordImportSpec.create(page, testInfo)
11+
await passwordImportFeature.enabled()
12+
await passwordImportFeature.navigate()
13+
const didAnimatePasswordOptions = passwordImportFeature.waitForAnimation('a[aria-label="Password options"]')
14+
await passwordImportFeature.clickOnElement('Home page')
15+
await didAnimatePasswordOptions
16+
17+
const didAnimateSignin = passwordImportFeature.waitForAnimation('a[aria-label="Sign in"]')
18+
await passwordImportFeature.clickOnElement('Signin page')
19+
await didAnimateSignin
20+
21+
const didAnimateExport = passwordImportFeature.waitForAnimation('button[aria-label="Export"]')
22+
await passwordImportFeature.clickOnElement('Export page')
23+
await didAnimateExport
24+
})
25+
26+
export class AutofillPasswordImportSpec {
27+
htmlPage = '/autofill-password-import/index.html'
28+
config = './integration-test/test-pages/autofill-password-import/config/config.json'
29+
/**
30+
* @param {import("@playwright/test").Page} page
31+
* @param {import("./type-helpers.mjs").Build} build
32+
* @param {import("./type-helpers.mjs").PlatformInfo} platform
33+
*/
34+
constructor (page, build, platform) {
35+
this.page = page
36+
this.build = build
37+
this.platform = platform
38+
}
39+
40+
async enabled () {
41+
const config = JSON.parse(readFileSync(this.config, 'utf8'))
42+
await this.setup({ config })
43+
}
44+
45+
async navigate () {
46+
await this.page.goto(this.htmlPage)
47+
}
48+
49+
/**
50+
* @param {object} params
51+
* @param {Record<string, any>} params.config
52+
* @return {Promise<void>}
53+
*/
54+
async setup (params) {
55+
const { config } = params
56+
57+
// read the built file from disk and do replacements
58+
const injectedJS = wrapWebkitScripts(this.build.artifact, {
59+
$CONTENT_SCOPE$: config,
60+
$USER_UNPROTECTED_DOMAINS$: [],
61+
$USER_PREFERENCES$: {
62+
platform: { name: 'android' },
63+
debug: true,
64+
javascriptInterface: '',
65+
messageCallback: '',
66+
sessionKey: ''
67+
}
68+
})
69+
70+
await this.page.addInitScript(mockAndroidMessaging, {
71+
messagingContext: {
72+
env: 'development',
73+
context: 'contentScopeScripts',
74+
featureName: 'n/a'
75+
},
76+
responses: {},
77+
messageCallback: ''
78+
})
79+
80+
// attach the JS
81+
await this.page.addInitScript(injectedJS)
82+
}
83+
84+
/**
85+
* Helper for creating an instance per platform
86+
* @param {import("@playwright/test").Page} page
87+
* @param {import("@playwright/test").TestInfo} testInfo
88+
*/
89+
static create (page, testInfo) {
90+
// Read the configuration object to determine which platform we're testing against
91+
const { platformInfo, build } = perPlatform(testInfo.project.use)
92+
return new AutofillPasswordImportSpec(page, build, platformInfo)
93+
}
94+
95+
/**
96+
* Helper to assert that an element is animating
97+
* @param {string} selector
98+
*/
99+
async waitForAnimation (selector) {
100+
const locator = await this.page.locator(selector)
101+
return await locator.evaluate((el) => {
102+
if (el != null) {
103+
return el.getAnimations().some((animation) => animation.playState === 'running')
104+
} else {
105+
return false
106+
}
107+
}, selector)
108+
}
109+
110+
/**
111+
* Helper to click on a button accessed via the aria-label attrbitue
112+
* @param {string} text
113+
*/
114+
async clickOnElement (text) {
115+
const element = await this.page.getByText(text)
116+
await element.click()
117+
}
118+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
{
2+
"features": {
3+
"autofillPasswordImport": {
4+
"state": "enabled",
5+
"exceptions": [],
6+
"settings": {
7+
"domains": [
8+
{
9+
"domain": [
10+
"passwords.google.com",
11+
"localhost"
12+
],
13+
"patchSettings": [
14+
{
15+
"path": "",
16+
"op": "add",
17+
"value": {
18+
"settingsButton": {
19+
"shouldAutotap": false,
20+
"path": "/",
21+
"selectors": [
22+
"a[href*='options']"
23+
],
24+
"labelTexts": [
25+
"Password options"
26+
]
27+
},
28+
"exportButton": {
29+
"shouldAutotap": false,
30+
"path": "/options",
31+
"selectors": [
32+
"c-wiz[data-node-index*='2;0'][data-p*='options']",
33+
"c-wiz[data-p*='options'][jsdata='deferred-i4']"
34+
],
35+
"labelTexts": [
36+
"Export"
37+
]
38+
},
39+
"signInButton": {
40+
"shouldAutotap": false,
41+
"path": "/intro",
42+
"selectors": [
43+
"a[href*='ServiceLogin']:not([target='_top'])",
44+
"a[aria-label='Sign in']:not([target='_top'])"
45+
],
46+
"labelTexts": [
47+
"Sign in"
48+
]
49+
}
50+
}
51+
}
52+
]
53+
}
54+
]
55+
}
56+
}
57+
},
58+
"unprotectedTemporary": []
59+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>A page with SPA navigation (like google password manager)</title>
7+
<p>
8+
This is an SPA like page, with some elements that appear on navigation using the pushState API.
9+
</p>
10+
<style>
11+
.body {
12+
display: flex;
13+
flex-direction: column;
14+
height: 100%;
15+
margin-bottom: 50px;
16+
align-items: center;
17+
justify-content: center;
18+
}
19+
.page {
20+
display: none;
21+
}
22+
.page.active {
23+
display: flex;
24+
height: 100%;
25+
justify-content: center;
26+
align-items: center;
27+
}
28+
</style>
29+
</head>
30+
<body class="body">
31+
<div id="intro" class="page">
32+
<a aria-label="Sign in">Sign in</a>
33+
</div>
34+
<div id="home" class="page">
35+
<a aria-label="Password options">Password options</a>
36+
</div>
37+
<div id="options" class="page">
38+
<button aria-label="Export">Export</button>
39+
</div>
40+
41+
<script>
42+
const routes = {
43+
'/intro': document.getElementById('intro'),
44+
'/': document.getElementById('home'),
45+
'/options': document.getElementById('options')
46+
};
47+
48+
function navigate(path) {
49+
window.history.pushState({}, path, window.location.origin + path);
50+
updatePage(path);
51+
}
52+
53+
function updatePage(path) {
54+
Object.values(routes).forEach(page => page.classList.remove('active'));
55+
if (routes[path]) {
56+
routes[path].classList.add('active');
57+
}
58+
}
59+
60+
window.onpopstate = () => updatePage(window.location.pathname);
61+
62+
// Initial load
63+
updatePage(window.location.pathname || '/');
64+
65+
// Example navigation buttons (you can remove these if not needed)
66+
document.body.insertAdjacentHTML('beforeend', `
67+
<nav>
68+
<button onclick="navigate('/intro')">Signin page</button>
69+
<button onclick="navigate('/')">Home page</button>
70+
<button onclick="navigate('/options')">Export page</button>
71+
</nav>
72+
`);
73+
</script>
74+
</body>
75+
</html>

injected/integration-test/type-helpers.mjs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ export class Build {
6060
windows: () => '../build/windows/contentScope.js',
6161
android: () => '../build/android/contentScope.js',
6262
'apple': () => '../Sources/ContentScopeScripts/dist/contentScope.js',
63-
'apple-isolated': () => '../Sources/ContentScopeScripts/dist/contentScopeIsolated.js'
63+
'apple-isolated': () => '../Sources/ContentScopeScripts/dist/contentScopeIsolated.js',
64+
'android-autofill-password-import': () => '../build/android/autofillPasswordImport.js'
6465
})
6566
return readFileSync(path, 'utf8')
6667
}
@@ -71,7 +72,7 @@ export class Build {
7172
*/
7273
static supported (name) {
7374
/** @type {ImportMeta['injectName'][]} */
74-
const items = ['apple', 'apple-isolated', 'windows', 'integration', 'android']
75+
const items = ['apple', 'apple-isolated', 'windows', 'integration', 'android', 'android-autofill-password-import']
7576
if (items.includes(name)) {
7677
return name
7778
}

injected/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"build-chrome": "node scripts/entry-points.js --platform chrome",
1111
"build-chrome-mv3": "node scripts/entry-points.js --platform chrome-mv3",
1212
"build-apple": "node scripts/entry-points.js --platform apple && node scripts/entry-points.js --platform apple-isolated",
13-
"build-android": "node scripts/entry-points.js --platform android",
13+
"build-android": "node scripts/entry-points.js --platform android && node scripts/entry-points.js --platform android-autofill-password-import",
1414
"build-windows": "node scripts/entry-points.js --platform windows",
1515
"build-integration": "node scripts/entry-points.js --platform integration",
1616
"build-types": "node scripts/types.mjs",

injected/playwright.config.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ export default defineConfig({
4444
],
4545
use: { injectName: 'android', platform: 'android', ...devices['Galaxy S5'] }
4646
},
47+
{
48+
name: 'android-autofill-password-import',
49+
testMatch: [
50+
'integration-test/autofill-password-import.spec.js'
51+
],
52+
use: { injectName: 'android-autofill-password-import', platform: 'android', ...devices['Galaxy S5'] }
53+
},
4754
{
4855
name: 'chrome',
4956
testMatch: [

injected/scripts/entry-points.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ const builds = {
3333
input: 'entry-points/android.js',
3434
output: ['../build/android/contentScope.js']
3535
},
36+
'android-autofill-password-import': {
37+
input: 'entry-points/android',
38+
output: ['../build/android/autofillPasswordImport.js']
39+
},
3640
windows: {
3741
input: 'entry-points/windows.js',
3842
output: ['../build/windows/contentScope.js']

injected/src/features.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ const otherFeatures = /** @type {const} */([
2222
'windowsPermissionUsage',
2323
'brokerProtection',
2424
'performanceMetrics',
25-
'breakageReporting'
25+
'breakageReporting',
26+
'autofillPasswordImport'
2627
])
2728

2829
/** @typedef {baseFeatures[number]|otherFeatures[number]} FeatureName */
@@ -45,6 +46,9 @@ export const platformSupport = {
4546
'breakageReporting',
4647
'duckPlayer'
4748
],
49+
'android-autofill-password-import': [
50+
'autofillPasswordImport'
51+
],
4852
windows: [
4953
'cookie',
5054
...baseFeatures,

0 commit comments

Comments
 (0)