feat: extensão de navegador para gerar CPF/CNPJ no menu de contexto
Adiciona uma extensão MV3 (Firefox e Chromium/Brave) que insere CPF ou CNPJ
válido direto no campo focado, pelo menu de contexto de qualquer input,
textarea ou contenteditable.
Extensões rodam em sandbox sem acesso a processos, então não há como invocar
os binários cpfgen/cnpjgen; fazer isso exigiria um host de native messaging
registrado no SO. Como os algoritmos são curtos e puros, foram portados para
JS em extension/generator.js, a partir de main.go (CPF) e de
cnpj_generator/cnpj/generate.go (CNPJ), trocando math/rand por
crypto.getRandomValues. test_generator.mjs compara as duas implementações
para que não divirjam.
A inserção usa document.execCommand("insertText"), que emite os eventos
nativos e portanto funciona em campos controlados por React/Vue — atribuir
.value diretamente seria descartado por esses frameworks.
Somente desktop: o Firefox para Android não implementa a API menus.
Testes carregam a extensão de verdade nos dois navegadores e verificam que os
8 itens de menu são criados e que a inserção funciona nos três tipos de campo.
This commit is contained in:
parent
688c5a48f3
commit
7632cfde2c
14 changed files with 792 additions and 4 deletions
141
extension/test_extension.py
Normal file
141
extension/test_extension.py
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
"""Carrega a extensão num Firefox headless via Marionette, confirma que o
|
||||
background script inicializou e que os itens de menu foram criados, e exercita
|
||||
insert.js contra um input real, um textarea e um contenteditable."""
|
||||
import json, os, socket, subprocess, tempfile, time, shutil, sys
|
||||
|
||||
EXT = os.path.dirname(os.path.abspath(__file__))
|
||||
PORT = 2929
|
||||
|
||||
class M:
|
||||
def __init__(self, port):
|
||||
self.s = socket.create_connection(("127.0.0.1", port), timeout=60)
|
||||
self.s.settimeout(60)
|
||||
self.buf = b""
|
||||
self._read() # handshake
|
||||
self.msgid = 0
|
||||
|
||||
def _read(self):
|
||||
while b":" not in self.buf:
|
||||
self.buf += self.s.recv(65536)
|
||||
n, _, rest = self.buf.partition(b":")
|
||||
n = int(n)
|
||||
while len(rest) < n:
|
||||
rest += self.s.recv(65536)
|
||||
self.buf = rest[n:]
|
||||
return json.loads(rest[:n])
|
||||
|
||||
def cmd(self, name, params=None):
|
||||
self.msgid += 1
|
||||
payload = json.dumps([0, self.msgid, name, params or {}]).encode()
|
||||
self.s.sendall(str(len(payload)).encode() + b":" + payload)
|
||||
while True:
|
||||
msg = self._read()
|
||||
if isinstance(msg, list) and msg[0] == 1 and msg[1] == self.msgid:
|
||||
if msg[2]:
|
||||
raise RuntimeError(f"{name}: {msg[2]}")
|
||||
return msg[3]
|
||||
|
||||
if not shutil.which("firefox"):
|
||||
print("firefox não encontrado, pulando")
|
||||
raise SystemExit(0)
|
||||
|
||||
profile = tempfile.mkdtemp(prefix="ffprof-")
|
||||
with open(os.path.join(profile, "user.js"), "w") as f:
|
||||
f.write('user_pref("xpinstall.signatures.required", false);\n')
|
||||
f.write('user_pref("extensions.autoDisableScopes", 0);\n')
|
||||
f.write('user_pref("marionette.port", %d);\n' % PORT)
|
||||
f.write('user_pref("browser.shell.checkDefaultBrowser", false);\n')
|
||||
|
||||
proc = subprocess.Popen(
|
||||
["firefox", "--marionette", "-remote-allow-system-access", "--headless", "--no-remote", "--profile", profile],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||
)
|
||||
|
||||
def cleanup():
|
||||
proc.terminate()
|
||||
try: proc.wait(timeout=10)
|
||||
except Exception: proc.kill()
|
||||
shutil.rmtree(profile, ignore_errors=True)
|
||||
|
||||
try:
|
||||
for _ in range(60):
|
||||
try:
|
||||
m = M(PORT); break
|
||||
except OSError:
|
||||
time.sleep(1)
|
||||
else:
|
||||
raise SystemExit("marionette não subiu")
|
||||
|
||||
m.cmd("WebDriver:NewSession", {"capabilities": {}})
|
||||
|
||||
res = m.cmd("Addon:Install", {"path": EXT, "temporary": True})
|
||||
addon_id = res.get("value", res)
|
||||
print("extensão instalada:", addon_id)
|
||||
time.sleep(3)
|
||||
|
||||
# Página de teste com os três tipos de campo editável.
|
||||
html = "file://" + os.path.join(EXT, "testdata.html")
|
||||
m.cmd("WebDriver:Navigate", {"url": html})
|
||||
|
||||
# Injeta insert.js no conteúdo e valida cada alvo.
|
||||
src = open(os.path.join(EXT, "insert.js")).read()
|
||||
script = src + """
|
||||
const out = [];
|
||||
for (const id of ["a","b","c"]) {
|
||||
const el = document.getElementById(id);
|
||||
el.focus();
|
||||
if (el.isContentEditable) {
|
||||
const r = document.createRange();
|
||||
r.selectNodeContents(el); r.collapse(false);
|
||||
const s = getSelection(); s.removeAllRanges(); s.addRange(r);
|
||||
}
|
||||
const ok = insertIntoFocused("123.456.789-09");
|
||||
const val = el.isContentEditable ? el.textContent : el.value;
|
||||
out.push(id + ":" + ok + ":" + val);
|
||||
}
|
||||
return out.join("|");
|
||||
"""
|
||||
r = m.cmd("WebDriver:ExecuteScript", {"script": script, "args": [], "newSandbox": True})
|
||||
inserted = r.get("value")
|
||||
print("inserção:", inserted)
|
||||
for part in inserted.split("|"):
|
||||
field, ok, value = part.split(":", 2)
|
||||
assert ok == "true", f"insert falhou em #{field}"
|
||||
assert "123.456.789-09" in value, f"valor ausente em #{field}: {value}"
|
||||
|
||||
# Confere, no processo da extensão, que os menus foram registrados.
|
||||
m.cmd("Marionette:SetContext", {"value": "chrome"})
|
||||
probe = """
|
||||
const { ExtensionParent } = ChromeUtils.importESModule(
|
||||
"resource://gre/modules/ExtensionParent.sys.mjs");
|
||||
const pol = WebExtensionPolicy.getByID("cpfgen@lucasf.xyz");
|
||||
if (!pol) return "SEM POLICY";
|
||||
const ctx = ExtensionParent.GlobalManager.getExtension("cpfgen@lucasf.xyz");
|
||||
const mm = ctx && ctx.apiManager;
|
||||
const menus = ChromeUtils.importESModule(
|
||||
"resource://gre/modules/ExtensionParent.sys.mjs");
|
||||
let ids = [];
|
||||
try {
|
||||
const m = ctx.apiManager.global.gMenuMap;
|
||||
const ext = [...m.keys()].find(e => e.id === "cpfgen@lucasf.xyz");
|
||||
ids = ext ? [...m.get(ext).keys()] : [];
|
||||
} catch (e) { ids = ["ERRO:" + e.message]; }
|
||||
return JSON.stringify({active: pol.active, bg: !!ctx, menus: ids});
|
||||
"""
|
||||
r2 = m.cmd("WebDriver:ExecuteScript", {"script": probe, "args": []})
|
||||
print("estado:", r2.get("value"))
|
||||
|
||||
state = json.loads(r2.get("value"))
|
||||
assert state["active"] and state["bg"], "extensão não ficou ativa"
|
||||
esperados = {
|
||||
"cpf", "cnpj",
|
||||
"cpf-formatted", "cpf-raw",
|
||||
"cnpj-formatted", "cnpj-raw",
|
||||
"cnpj-alpha-formatted", "cnpj-alpha-raw",
|
||||
}
|
||||
faltando = esperados - set(state["menus"])
|
||||
assert not faltando, f"itens de menu ausentes: {faltando}"
|
||||
|
||||
print("\nOK: extensão ativa, 8 itens de menu, inserção nos 3 tipos de campo")
|
||||
finally:
|
||||
cleanup()
|
||||
Loading…
Add table
Add a link
Reference in a new issue