Skip to content

Commit

Permalink
[Google password manager] Android isolated module for password import (
Browse files Browse the repository at this point in the history
…#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]>
  • Loading branch information
dbajpeyi and jonathanKingston authored Oct 29, 2024
1 parent a44aaef commit 0454d7d
Show file tree
Hide file tree
Showing 11 changed files with 559 additions and 5 deletions.
118 changes: 118 additions & 0 deletions injected/integration-test/autofill-password-import.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { test } from '@playwright/test'
import { readFileSync } from 'fs'
import {
mockAndroidMessaging,
wrapWebkitScripts
} from '@duckduckgo/messaging/lib/test-utils.mjs'
import { perPlatform } from './type-helpers.mjs'

test('Password import feature', async ({ page }, testInfo) => {
const passwordImportFeature = AutofillPasswordImportSpec.create(page, testInfo)
await passwordImportFeature.enabled()
await passwordImportFeature.navigate()
const didAnimatePasswordOptions = passwordImportFeature.waitForAnimation('a[aria-label="Password options"]')
await passwordImportFeature.clickOnElement('Home page')
await didAnimatePasswordOptions

const didAnimateSignin = passwordImportFeature.waitForAnimation('a[aria-label="Sign in"]')
await passwordImportFeature.clickOnElement('Signin page')
await didAnimateSignin

const didAnimateExport = passwordImportFeature.waitForAnimation('button[aria-label="Export"]')
await passwordImportFeature.clickOnElement('Export page')
await didAnimateExport
})

export class AutofillPasswordImportSpec {
htmlPage = '/autofill-password-import/index.html'
config = './integration-test/test-pages/autofill-password-import/config/config.json'
/**
* @param {import("@playwright/test").Page} page
* @param {import("./type-helpers.mjs").Build} build
* @param {import("./type-helpers.mjs").PlatformInfo} platform
*/
constructor (page, build, platform) {
this.page = page
this.build = build
this.platform = platform
}

async enabled () {
const config = JSON.parse(readFileSync(this.config, 'utf8'))
await this.setup({ config })
}

async navigate () {
await this.page.goto(this.htmlPage)
}

/**
* @param {object} params
* @param {Record<string, any>} params.config
* @return {Promise<void>}
*/
async setup (params) {
const { config } = params

// read the built file from disk and do replacements
const injectedJS = wrapWebkitScripts(this.build.artifact, {
$CONTENT_SCOPE$: config,
$USER_UNPROTECTED_DOMAINS$: [],
$USER_PREFERENCES$: {
platform: { name: 'android' },
debug: true,
javascriptInterface: '',
messageCallback: '',
sessionKey: ''
}
})

await this.page.addInitScript(mockAndroidMessaging, {
messagingContext: {
env: 'development',
context: 'contentScopeScripts',
featureName: 'n/a'
},
responses: {},
messageCallback: ''
})

// attach the JS
await this.page.addInitScript(injectedJS)
}

/**
* Helper for creating an instance per platform
* @param {import("@playwright/test").Page} page
* @param {import("@playwright/test").TestInfo} testInfo
*/
static create (page, testInfo) {
// Read the configuration object to determine which platform we're testing against
const { platformInfo, build } = perPlatform(testInfo.project.use)
return new AutofillPasswordImportSpec(page, build, platformInfo)
}

/**
* Helper to assert that an element is animating
* @param {string} selector
*/
async waitForAnimation (selector) {
const locator = await this.page.locator(selector)
return await locator.evaluate((el) => {
if (el != null) {
return el.getAnimations().some((animation) => animation.playState === 'running')
} else {
return false
}
}, selector)
}

/**
* Helper to click on a button accessed via the aria-label attrbitue
* @param {string} text
*/
async clickOnElement (text) {
const element = await this.page.getByText(text)
await element.click()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{
"features": {
"autofillPasswordImport": {
"state": "enabled",
"exceptions": [],
"settings": {
"domains": [
{
"domain": [
"passwords.google.com",
"localhost"
],
"patchSettings": [
{
"path": "",
"op": "add",
"value": {
"settingsButton": {
"shouldAutotap": false,
"path": "/",
"selectors": [
"a[href*='options']"
],
"labelTexts": [
"Password options"
]
},
"exportButton": {
"shouldAutotap": false,
"path": "/options",
"selectors": [
"c-wiz[data-node-index*='2;0'][data-p*='options']",
"c-wiz[data-p*='options'][jsdata='deferred-i4']"
],
"labelTexts": [
"Export"
]
},
"signInButton": {
"shouldAutotap": false,
"path": "/intro",
"selectors": [
"a[href*='ServiceLogin']:not([target='_top'])",
"a[aria-label='Sign in']:not([target='_top'])"
],
"labelTexts": [
"Sign in"
]
}
}
}
]
}
]
}
}
},
"unprotectedTemporary": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>A page with SPA navigation (like google password manager)</title>
<p>
This is an SPA like page, with some elements that appear on navigation using the pushState API.
</p>
<style>
.body {
display: flex;
flex-direction: column;
height: 100%;
margin-bottom: 50px;
align-items: center;
justify-content: center;
}
.page {
display: none;
}
.page.active {
display: flex;
height: 100%;
justify-content: center;
align-items: center;
}
</style>
</head>
<body class="body">
<div id="intro" class="page">
<a aria-label="Sign in">Sign in</a>
</div>
<div id="home" class="page">
<a aria-label="Password options">Password options</a>
</div>
<div id="options" class="page">
<button aria-label="Export">Export</button>
</div>

<script>
const routes = {
'/intro': document.getElementById('intro'),
'/': document.getElementById('home'),
'/options': document.getElementById('options')
};

function navigate(path) {
window.history.pushState({}, path, window.location.origin + path);
updatePage(path);
}

function updatePage(path) {
Object.values(routes).forEach(page => page.classList.remove('active'));
if (routes[path]) {
routes[path].classList.add('active');
}
}

window.onpopstate = () => updatePage(window.location.pathname);

// Initial load
updatePage(window.location.pathname || '/');

// Example navigation buttons (you can remove these if not needed)
document.body.insertAdjacentHTML('beforeend', `
<nav>
<button onclick="navigate('/intro')">Signin page</button>
<button onclick="navigate('/')">Home page</button>
<button onclick="navigate('/options')">Export page</button>
</nav>
`);
</script>
</body>
</html>
5 changes: 3 additions & 2 deletions injected/integration-test/type-helpers.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ export class Build {
windows: () => '../build/windows/contentScope.js',
android: () => '../build/android/contentScope.js',
'apple': () => '../Sources/ContentScopeScripts/dist/contentScope.js',
'apple-isolated': () => '../Sources/ContentScopeScripts/dist/contentScopeIsolated.js'
'apple-isolated': () => '../Sources/ContentScopeScripts/dist/contentScopeIsolated.js',
'android-autofill-password-import': () => '../build/android/autofillPasswordImport.js'
})
return readFileSync(path, 'utf8')
}
Expand All @@ -71,7 +72,7 @@ export class Build {
*/
static supported (name) {
/** @type {ImportMeta['injectName'][]} */
const items = ['apple', 'apple-isolated', 'windows', 'integration', 'android']
const items = ['apple', 'apple-isolated', 'windows', 'integration', 'android', 'android-autofill-password-import']
if (items.includes(name)) {
return name
}
Expand Down
2 changes: 1 addition & 1 deletion injected/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"build-chrome": "node scripts/entry-points.js --platform chrome",
"build-chrome-mv3": "node scripts/entry-points.js --platform chrome-mv3",
"build-apple": "node scripts/entry-points.js --platform apple && node scripts/entry-points.js --platform apple-isolated",
"build-android": "node scripts/entry-points.js --platform android",
"build-android": "node scripts/entry-points.js --platform android && node scripts/entry-points.js --platform android-autofill-password-import",
"build-windows": "node scripts/entry-points.js --platform windows",
"build-integration": "node scripts/entry-points.js --platform integration",
"build-types": "node scripts/types.mjs",
Expand Down
7 changes: 7 additions & 0 deletions injected/playwright.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ export default defineConfig({
],
use: { injectName: 'android', platform: 'android', ...devices['Galaxy S5'] }
},
{
name: 'android-autofill-password-import',
testMatch: [
'integration-test/autofill-password-import.spec.js'
],
use: { injectName: 'android-autofill-password-import', platform: 'android', ...devices['Galaxy S5'] }
},
{
name: 'chrome',
testMatch: [
Expand Down
4 changes: 4 additions & 0 deletions injected/scripts/entry-points.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ const builds = {
input: 'entry-points/android.js',
output: ['../build/android/contentScope.js']
},
'android-autofill-password-import': {
input: 'entry-points/android',
output: ['../build/android/autofillPasswordImport.js']
},
windows: {
input: 'entry-points/windows.js',
output: ['../build/windows/contentScope.js']
Expand Down
6 changes: 5 additions & 1 deletion injected/src/features.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ const otherFeatures = /** @type {const} */([
'windowsPermissionUsage',
'brokerProtection',
'performanceMetrics',
'breakageReporting'
'breakageReporting',
'autofillPasswordImport'
])

/** @typedef {baseFeatures[number]|otherFeatures[number]} FeatureName */
Expand All @@ -45,6 +46,9 @@ export const platformSupport = {
'breakageReporting',
'duckPlayer'
],
'android-autofill-password-import': [
'autofillPasswordImport'
],
windows: [
'cookie',
...baseFeatures,
Expand Down
Loading

0 comments on commit 0454d7d

Please sign in to comment.