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.
141 lines
5.2 KiB
Python
141 lines
5.2 KiB
Python
"""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()
|