[{"data":1,"prerenderedAt":1202},["ShallowReactive",2],{"\u002Fposts\u002Fyoutube-like-shortcut-tests-con-capturas-reales\u002F":3,"upcoming:\u002Fposts\u002Fyoutube-like-shortcut-tests-con-capturas-reales\u002F":1201},{"id":4,"title":5,"body":6,"date":1186,"description":1187,"draft":1188,"extension":1189,"meta":1190,"navigation":590,"path":1191,"seo":1192,"stem":1193,"tags":1194,"type":1199,"__hash__":1200},"posts\u002Fposts\u002Fyoutube-like-shortcut-tests-con-capturas-reales.md","YouTube Like Shortcut 1.5.1: tests con el HTML real de YouTube",{"type":7,"value":8,"toc":1172},"minimark",[9,19,27,32,40,53,58,73,76,208,214,408,411,414,418,421,438,461,479,483,486,684,693,697,707,785,789,808,888,892,895,1117,1126,1130,1141,1144,1148,1156,1159,1168],[10,11,12,13,18],"p",{},"En ",[14,15,17],"a",{"href":16},"\u002Fposts\u002Fyoutube-like-shortcut-por-dentro","YouTube Like Shortcut por dentro"," terminé con una lista de lo que haría distinto. Lo primero eran tests automáticos. Lo segundo, que encontrar el botón no dependiera del idioma de YouTube. Lo tercero, cambiar los tiempos fijos por esperar a que el botón esté listo.",[10,20,21,22,26],{},"La versión 1.5.1 resuelve los dos primeros y, de paso, seis fallos que no sabía que tenía. Si buscas qué hace la extensión y cómo se usa, eso está en el ",[14,23,25],{"href":24},"\u002Fposts\u002Fyoutube-like-shortcut-like-con-una-tecla","artículo para usuarios",".",[28,29,31],"h2",{"id":30},"tests-con-capturas-no-con-html-inventado","Tests con capturas, no con HTML inventado",[10,33,34,35,39],{},"La tentación era escribir a mano un HTML que se pareciera al de YouTube y probar contra él. El problema es que un test así comprueba que el código funciona con lo que ",[36,37,38],"em",{},"yo creo"," que sirve YouTube, que es justo lo que puede estar mal.",[10,41,42,43,47,48,52],{},"Así que los tests cargan ",[44,45,46],"strong",{},"capturas reales",". Un fragmento de código que se pega en la consola de DevTools copia la zona del botón de like de la página (el Short o la cabecera del vídeo) y la descarga como ",[49,50,51],"code",{},".html",". Hice nueve: vídeos con la ventana ancha y estrecha, con y sin like, un Short, un directo y YouTube en alemán.",[54,55,57],"h3",{"id":56},"happy-dom-no-sabe-medir","happy-dom no sabe medir",[10,59,60,61,64,65,68,69,72],{},"Los tests corren en ",[44,62,63],{},"happy-dom",", un DOM simulado sin navegador. No calcula el layout, así que ",[49,66,67],{},"getBoundingClientRect()"," devuelve ",[49,70,71],{},"0 × 0"," para todo. Y la extensión usa justo eso para descartar las variantes ocultas del botón: en los tests, todos los botones parecerían ocultos.",[10,74,75],{},"La solución fue guardar el tamaño real en la propia captura. Antes de copiar el HTML, el fragmento mide cada botón y lo apunta en un atributo:",[77,78,83],"pre",{"className":79,"code":80,"language":81,"meta":82,"style":82},"language-js shiki shiki-themes github-light github-dark","botones.forEach(b => {\n  const r = b.getBoundingClientRect()\n  b.setAttribute('data-fixture-rect', `${Math.round(r.width)}x${Math.round(r.height)}`)\n})\n","js","",[49,84,85,112,134,202],{"__ignoreMap":82},[86,87,90,94,98,101,105,109],"span",{"class":88,"line":89},"line",1,[86,91,93],{"class":92},"sVt8B","botones.",[86,95,97],{"class":96},"sScJk","forEach",[86,99,100],{"class":92},"(",[86,102,104],{"class":103},"s4XuR","b",[86,106,108],{"class":107},"szBVR"," =>",[86,110,111],{"class":92}," {\n",[86,113,115,118,122,125,128,131],{"class":88,"line":114},2,[86,116,117],{"class":107},"  const",[86,119,121],{"class":120},"sj4cs"," r",[86,123,124],{"class":107}," =",[86,126,127],{"class":92}," b.",[86,129,130],{"class":96},"getBoundingClientRect",[86,132,133],{"class":92},"()\n",[86,135,137,140,143,145,149,152,155,158,160,163,165,168,170,173,176,179,181,183,185,187,189,191,194,196,199],{"class":88,"line":136},3,[86,138,139],{"class":92},"  b.",[86,141,142],{"class":96},"setAttribute",[86,144,100],{"class":92},[86,146,148],{"class":147},"sZZnC","'data-fixture-rect'",[86,150,151],{"class":92},", ",[86,153,154],{"class":147},"`${",[86,156,157],{"class":92},"Math",[86,159,26],{"class":147},[86,161,162],{"class":96},"round",[86,164,100],{"class":147},[86,166,167],{"class":92},"r",[86,169,26],{"class":147},[86,171,172],{"class":92},"width",[86,174,175],{"class":147},")",[86,177,178],{"class":147},"}x${",[86,180,157],{"class":92},[86,182,26],{"class":147},[86,184,162],{"class":96},[86,186,100],{"class":147},[86,188,167],{"class":92},[86,190,26],{"class":147},[86,192,193],{"class":92},"height",[86,195,175],{"class":147},[86,197,198],{"class":147},"}`",[86,200,201],{"class":92},")\n",[86,203,205],{"class":88,"line":204},4,[86,206,207],{"class":92},"})\n",[10,209,210,211,213],{},"En los tests, ",[49,212,67],{}," se sustituye por una versión que lee ese atributo:",[77,215,219],{"className":216,"code":217,"language":218,"meta":82,"style":82},"language-ts shiki shiki-themes github-light github-dark","vi.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(function (this: Element) {\n  const match = \u002F^(\\d+)x(\\d+)$\u002F.exec(this.getAttribute('data-fixture-rect') ?? '')\n  const width = match ? Number(match[1]) : 0\n  const height = match ? Number(match[2]) : 0\n  \u002F\u002F ...\n})\n","ts",[49,220,221,270,338,370,396,403],{"__ignoreMap":82},[86,222,223,226,229,231,234,236,239,241,244,247,250,252,255,258,261,264,267],{"class":88,"line":89},[86,224,225],{"class":92},"vi.",[86,227,228],{"class":96},"spyOn",[86,230,100],{"class":92},[86,232,233],{"class":120},"Element",[86,235,26],{"class":92},[86,237,238],{"class":120},"prototype",[86,240,151],{"class":92},[86,242,243],{"class":147},"'getBoundingClientRect'",[86,245,246],{"class":92},").",[86,248,249],{"class":96},"mockImplementation",[86,251,100],{"class":92},[86,253,254],{"class":107},"function",[86,256,257],{"class":92}," (",[86,259,260],{"class":120},"this",[86,262,263],{"class":107},":",[86,265,266],{"class":96}," Element",[86,268,269],{"class":92},") {\n",[86,271,272,274,277,279,282,285,288,291,294,297,299,301,303,306,309,311,314,316,318,320,323,325,327,330,333,336],{"class":88,"line":114},[86,273,117],{"class":107},[86,275,276],{"class":120}," match",[86,278,124],{"class":107},[86,280,281],{"class":147}," \u002F",[86,283,284],{"class":107},"^",[86,286,100],{"class":287},"sA_wV",[86,289,290],{"class":120},"\\d",[86,292,293],{"class":107},"+",[86,295,296],{"class":287},")x(",[86,298,290],{"class":120},[86,300,293],{"class":107},[86,302,175],{"class":287},[86,304,305],{"class":107},"$",[86,307,308],{"class":147},"\u002F",[86,310,26],{"class":92},[86,312,313],{"class":96},"exec",[86,315,100],{"class":92},[86,317,260],{"class":120},[86,319,26],{"class":92},[86,321,322],{"class":96},"getAttribute",[86,324,100],{"class":92},[86,326,148],{"class":147},[86,328,329],{"class":92},") ",[86,331,332],{"class":107},"??",[86,334,335],{"class":147}," ''",[86,337,201],{"class":92},[86,339,340,342,345,347,350,353,356,359,362,365,367],{"class":88,"line":136},[86,341,117],{"class":107},[86,343,344],{"class":120}," width",[86,346,124],{"class":107},[86,348,349],{"class":92}," match ",[86,351,352],{"class":107},"?",[86,354,355],{"class":96}," Number",[86,357,358],{"class":92},"(match[",[86,360,361],{"class":120},"1",[86,363,364],{"class":92},"]) ",[86,366,263],{"class":107},[86,368,369],{"class":120}," 0\n",[86,371,372,374,377,379,381,383,385,387,390,392,394],{"class":88,"line":204},[86,373,117],{"class":107},[86,375,376],{"class":120}," height",[86,378,124],{"class":107},[86,380,349],{"class":92},[86,382,352],{"class":107},[86,384,355],{"class":96},[86,386,358],{"class":92},[86,388,389],{"class":120},"2",[86,391,364],{"class":92},[86,393,263],{"class":107},[86,395,369],{"class":120},[86,397,399],{"class":88,"line":398},5,[86,400,402],{"class":401},"sJ8bj","  \u002F\u002F ...\n",[86,404,406],{"class":88,"line":405},6,[86,407,207],{"class":92},[10,409,410],{},"Así el test ve los botones con el tamaño que tenían en mi pantalla al hacer la captura: el visible mide 70 × 40 y las variantes ocultas, 0 × 0.",[10,412,413],{},"Estos tests no avisan solos cuando YouTube cambia. Su trabajo es otro: cuando algo falle, hago una captura nueva, la añado y el test me dice qué búsqueda ha dejado de funcionar. Y a partir de ahí, ese arreglo queda protegido.",[28,415,417],{"id":416},"lo-que-encontraron-las-capturas","Lo que encontraron las capturas",[10,419,420],{},"Con las capturas delante, lo primero que vi es que el artículo anterior describía una extensión que ya no funcionaba como yo creía.",[10,422,423,426,427,430,431,434,435,26],{},[44,424,425],{},"Shorts."," El artículo decía que el Short que estás viendo lleva el atributo ",[49,428,429],{},"is-active"," y que el like está en ",[49,432,433],{},"#like-button",". Ya no existe ninguno de los dos. YouTube tiene ahora una sola barra de acciones compartida por todo el feed, y el botón vive dentro de un componente ",[49,436,437],{},"like-button-view-model",[10,439,440,441,444,445,448,449,452,453,456,457,460],{},"La extensión seguía funcionando, pero por los pelos. Caía en los planes de respaldo hasta llegar al último: buscar un botón cuyo ",[49,442,443],{},"aria-label"," dijera ",[36,446,447],{},"like"," o ",[36,450,451],{},"me gusta",". En alemán funcionaba de casualidad, porque la etiqueta contiene ",[36,454,455],{},"liken",". En francés (",[36,458,459],{},"J'aime",") o en japonés no habría encontrado nada.",[10,462,463,466,467,470,471,474,475,478],{},[44,464,465],{},"Vídeos normales."," De las cuatro estrategias, ",[49,468,469],{},"#segmented-like-button"," y ",[49,472,473],{},"ytd-toggle-button-renderer"," no aparecen en ninguna captura. La que funciona es la segunda, ",[49,476,477],{},"like-button-view-model button",", tanto con 2029 px de ancho como con 268. Las otras se quedan por si alguna versión de YouTube las sigue sirviendo, con un test sintético cada una.",[28,480,482],{"id":481},"shorts-el-botón-por-su-componente","Shorts: el botón por su componente",[10,484,485],{},"La búsqueda en los Shorts ahora va por estructura y deja el texto como último recurso:",[77,487,489],{"className":216,"code":488,"language":218,"meta":82,"style":82},"export function findShortLikeButton(root: ParentNode = document): HTMLElement | null {\n  \u002F\u002F Strategy 1: Legacy layout (active renderer with #like-button)\n  const legacy = root.querySelector('ytd-reel-video-renderer[is-active] #like-button button')\n  if (legacy && isButtonInteractable(legacy)) return legacy as HTMLElement\n\n  \u002F\u002F Strategy 2: like-button-view-model (current component system); hidden variants are 0x0\n  const viewModelBtns = [...root.querySelectorAll('ytd-shorts like-button-view-model button')]\n  const viewModelMatch = pickOnScreen(viewModelBtns.filter(isButtonInteractable))\n  if (viewModelMatch) return viewModelMatch\n\n  \u002F\u002F Strategy 3: Aria label search inside the Shorts player\n  \u002F\u002F ...\n}\n",[49,490,491,530,535,557,586,592,597,627,649,662,667,673,678],{"__ignoreMap":82},[86,492,493,496,499,502,504,507,509,512,514,517,519,522,525,528],{"class":88,"line":89},[86,494,495],{"class":107},"export",[86,497,498],{"class":107}," function",[86,500,501],{"class":96}," findShortLikeButton",[86,503,100],{"class":92},[86,505,506],{"class":103},"root",[86,508,263],{"class":107},[86,510,511],{"class":96}," ParentNode",[86,513,124],{"class":107},[86,515,516],{"class":92}," document)",[86,518,263],{"class":107},[86,520,521],{"class":96}," HTMLElement",[86,523,524],{"class":107}," |",[86,526,527],{"class":120}," null",[86,529,111],{"class":92},[86,531,532],{"class":88,"line":114},[86,533,534],{"class":401},"  \u002F\u002F Strategy 1: Legacy layout (active renderer with #like-button)\n",[86,536,537,539,542,544,547,550,552,555],{"class":88,"line":136},[86,538,117],{"class":107},[86,540,541],{"class":120}," legacy",[86,543,124],{"class":107},[86,545,546],{"class":92}," root.",[86,548,549],{"class":96},"querySelector",[86,551,100],{"class":92},[86,553,554],{"class":147},"'ytd-reel-video-renderer[is-active] #like-button button'",[86,556,201],{"class":92},[86,558,559,562,565,568,571,574,577,580,583],{"class":88,"line":204},[86,560,561],{"class":107},"  if",[86,563,564],{"class":92}," (legacy ",[86,566,567],{"class":107},"&&",[86,569,570],{"class":96}," isButtonInteractable",[86,572,573],{"class":92},"(legacy)) ",[86,575,576],{"class":107},"return",[86,578,579],{"class":92}," legacy ",[86,581,582],{"class":107},"as",[86,584,585],{"class":96}," HTMLElement\n",[86,587,588],{"class":88,"line":398},[86,589,591],{"emptyLinePlaceholder":590},true,"\n",[86,593,594],{"class":88,"line":405},[86,595,596],{"class":401},"  \u002F\u002F Strategy 2: like-button-view-model (current component system); hidden variants are 0x0\n",[86,598,600,602,605,607,610,613,616,619,621,624],{"class":88,"line":599},7,[86,601,117],{"class":107},[86,603,604],{"class":120}," viewModelBtns",[86,606,124],{"class":107},[86,608,609],{"class":92}," [",[86,611,612],{"class":107},"...",[86,614,615],{"class":92},"root.",[86,617,618],{"class":96},"querySelectorAll",[86,620,100],{"class":92},[86,622,623],{"class":147},"'ytd-shorts like-button-view-model button'",[86,625,626],{"class":92},")]\n",[86,628,630,632,635,637,640,643,646],{"class":88,"line":629},8,[86,631,117],{"class":107},[86,633,634],{"class":120}," viewModelMatch",[86,636,124],{"class":107},[86,638,639],{"class":96}," pickOnScreen",[86,641,642],{"class":92},"(viewModelBtns.",[86,644,645],{"class":96},"filter",[86,647,648],{"class":92},"(isButtonInteractable))\n",[86,650,652,654,657,659],{"class":88,"line":651},9,[86,653,561],{"class":107},[86,655,656],{"class":92}," (viewModelMatch) ",[86,658,576],{"class":107},[86,660,661],{"class":92}," viewModelMatch\n",[86,663,665],{"class":88,"line":664},10,[86,666,591],{"emptyLinePlaceholder":590},[86,668,670],{"class":88,"line":669},11,[86,671,672],{"class":401},"  \u002F\u002F Strategy 3: Aria label search inside the Shorts player\n",[86,674,676],{"class":88,"line":675},12,[86,677,402],{"class":401},[86,679,681],{"class":88,"line":680},13,[86,682,683],{"class":92},"}\n",[10,685,686,687,692],{},"Hay un test que coge una captura de Shorts, ",[44,688,689,690],{},"borra todas las etiquetas ",[49,691,443],{}," y comprueba que el botón sigue apareciendo. Si pasa sin texto, pasa en cualquier idioma.",[54,694,696],{"id":695},"el-short-que-está-en-pantalla","El Short que está en pantalla",[10,698,699,700,702,703,706],{},"Al pasar de Short en Short, los anteriores siguen montados en la página, a tamaño completo, solo que por encima de la parte visible. Quedarse con el primer botón que mida más de ",[49,701,71],{}," podía dar like a un Short que ya habías pasado. ",[49,704,705],{},"pickOnScreen"," elige el que está dentro de la ventana:",[77,708,710],{"className":216,"code":709,"language":218,"meta":82,"style":82},"export function isInViewport(el: Element): boolean {\n  const rect = el.getBoundingClientRect()\n  return rect.bottom > 0 && rect.top \u003C window.innerHeight\n}\n",[49,711,712,739,755,781],{"__ignoreMap":82},[86,713,714,716,718,721,723,726,728,730,732,734,737],{"class":88,"line":89},[86,715,495],{"class":107},[86,717,498],{"class":107},[86,719,720],{"class":96}," isInViewport",[86,722,100],{"class":92},[86,724,725],{"class":103},"el",[86,727,263],{"class":107},[86,729,266],{"class":96},[86,731,175],{"class":92},[86,733,263],{"class":107},[86,735,736],{"class":120}," boolean",[86,738,111],{"class":92},[86,740,741,743,746,748,751,753],{"class":88,"line":114},[86,742,117],{"class":107},[86,744,745],{"class":120}," rect",[86,747,124],{"class":107},[86,749,750],{"class":92}," el.",[86,752,130],{"class":96},[86,754,133],{"class":92},[86,756,757,760,763,766,769,772,775,778],{"class":88,"line":136},[86,758,759],{"class":107},"  return",[86,761,762],{"class":92}," rect.bottom ",[86,764,765],{"class":107},">",[86,767,768],{"class":120}," 0",[86,770,771],{"class":107}," &&",[86,773,774],{"class":92}," rect.top ",[86,776,777],{"class":107},"\u003C",[86,779,780],{"class":92}," window.innerHeight\n",[86,782,783],{"class":88,"line":204},[86,784,683],{"class":92},[28,786,788],{"id":787},"seis-fallos-que-salieron-al-revisar","Seis fallos que salieron al revisar",[10,790,791,792,151,795,151,798,151,801,151,804,807],{},"Para escribir los tests tuve que sacar la lógica del content script a módulos pequeños (",[49,793,794],{},"like-button",[49,796,797],{},"shortcut",[49,799,800],{},"typing-guard",[49,802,803],{},"click",[49,805,806],{},"sound",") que reciben lo que necesitan como parámetros. Al leer el código pieza a pieza salieron fallos que nunca había visto. Cada uno se arregló con su test primero.",[809,810,811,834,843,852,869,879],"ol",{},[812,813,814,817,818,821,822,825,826,829,830,833],"li",{},[44,815,816],{},"Quitar un like se celebraba como un like."," Si ya le habías dado like y pulsabas ",[49,819,820],{},"L",", se quitaba, pero sonaba el ",[36,823,824],{},"pop",", salía el corazón y el contador de likes subía. Ahora la extensión mira ",[49,827,828],{},"aria-pressed"," ",[36,831,832],{},"antes"," del clic: si ya estaba pulsado, no hay sonido, ni animación, ni contador.",[812,835,836,839,840,842],{},[44,837,838],{},"Los Shorts no comprobaban el like."," La verificación con ",[49,841,828],{}," y el reintento que conté en el artículo anterior solo se hacían en vídeos normales. Ahora los Shorts usan la misma función.",[812,844,845,848,849,851],{},[44,846,847],{},"La tecla desaparecía fuera de los vídeos."," En la portada, en un canal o en los resultados de búsqueda, la extensión también cancelaba la ",[49,850,820],{},". Ahora la tecla solo se intercepta en Shorts, vídeos y directos.",[812,853,854,857,858,861,862,865,866,26],{},[44,855,856],{},"Los directos no tenían atajo."," Un directo vive en ",[49,859,860],{},"\u002Flive\u002F\u003Cid>",", no en ",[49,863,864],{},"\u002Fwatch",", y la extensión no lo reconocía. La captura del directo mostró que el botón es el mismo componente que en un vídeo, así que ahora se trata igual, también con la opción ",[44,867,868],{},"Solo en Shorts",[812,870,871,878],{},[44,872,873,874,877],{},"Un ",[49,875,876],{},"AudioContext"," por cada like."," El sonido creaba un contexto de audio nuevo en cada pulsación, y los navegadores limitan cuántos puede tener una página. En una sesión larga, el sonido acababa por no sonar. Ahora hay uno solo, que se crea la primera vez y se reutiliza.",[812,880,881,884,885,887],{},[44,882,883],{},"Reintentar sin poder comprobar."," Si el botón no tiene ",[49,886,828],{},", no hay forma de saber si el clic ha entrado, y un reintento podría quitar el like que acababas de dar. En ese caso, ahora no se reintenta.",[54,889,891],{"id":890},"decidir-qué-hacer-con-la-tecla","Decidir qué hacer con la tecla",[10,893,894],{},"El tercer fallo obligó a ordenar algo que antes estaba repartido por el manejador del teclado. Ahora es una función pura que recibe la situación y devuelve qué hacer:",[77,896,898],{"className":216,"code":897,"language":218,"meta":82,"style":82},"export function decideKeyAction(input: {\n  matches: boolean\n  typing: boolean\n  pageType: PageType\n  onlyShorts: boolean\n  navigating: boolean\n  repeat: boolean\n}): KeyAction {\n  if (!input.matches) return 'ignore'\n  if (input.typing) return 'ignore'\n  if (input.pageType === 'other') return 'ignore'\n  if (input.pageType === 'watch' && input.onlyShorts) return 'ignore'\n\n  if (input.navigating) return 'swallow'\n  if (input.repeat) return 'swallow'\n\n  return input.pageType === 'shorts' ? 'like-short' : 'like-watch'\n}\n",[49,899,900,918,928,937,947,956,965,974,986,1003,1014,1033,1053,1057,1070,1082,1087,1112],{"__ignoreMap":82},[86,901,902,904,906,909,911,914,916],{"class":88,"line":89},[86,903,495],{"class":107},[86,905,498],{"class":107},[86,907,908],{"class":96}," decideKeyAction",[86,910,100],{"class":92},[86,912,913],{"class":103},"input",[86,915,263],{"class":107},[86,917,111],{"class":92},[86,919,920,923,925],{"class":88,"line":114},[86,921,922],{"class":103},"  matches",[86,924,263],{"class":107},[86,926,927],{"class":120}," boolean\n",[86,929,930,933,935],{"class":88,"line":136},[86,931,932],{"class":103},"  typing",[86,934,263],{"class":107},[86,936,927],{"class":120},[86,938,939,942,944],{"class":88,"line":204},[86,940,941],{"class":103},"  pageType",[86,943,263],{"class":107},[86,945,946],{"class":96}," PageType\n",[86,948,949,952,954],{"class":88,"line":398},[86,950,951],{"class":103},"  onlyShorts",[86,953,263],{"class":107},[86,955,927],{"class":120},[86,957,958,961,963],{"class":88,"line":405},[86,959,960],{"class":103},"  navigating",[86,962,263],{"class":107},[86,964,927],{"class":120},[86,966,967,970,972],{"class":88,"line":599},[86,968,969],{"class":103},"  repeat",[86,971,263],{"class":107},[86,973,927],{"class":120},[86,975,976,979,981,984],{"class":88,"line":629},[86,977,978],{"class":92},"})",[86,980,263],{"class":107},[86,982,983],{"class":96}," KeyAction",[86,985,111],{"class":92},[86,987,988,990,992,995,998,1000],{"class":88,"line":651},[86,989,561],{"class":107},[86,991,257],{"class":92},[86,993,994],{"class":107},"!",[86,996,997],{"class":92},"input.matches) ",[86,999,576],{"class":107},[86,1001,1002],{"class":147}," 'ignore'\n",[86,1004,1005,1007,1010,1012],{"class":88,"line":664},[86,1006,561],{"class":107},[86,1008,1009],{"class":92}," (input.typing) ",[86,1011,576],{"class":107},[86,1013,1002],{"class":147},[86,1015,1016,1018,1021,1024,1027,1029,1031],{"class":88,"line":669},[86,1017,561],{"class":107},[86,1019,1020],{"class":92}," (input.pageType ",[86,1022,1023],{"class":107},"===",[86,1025,1026],{"class":147}," 'other'",[86,1028,329],{"class":92},[86,1030,576],{"class":107},[86,1032,1002],{"class":147},[86,1034,1035,1037,1039,1041,1044,1046,1049,1051],{"class":88,"line":675},[86,1036,561],{"class":107},[86,1038,1020],{"class":92},[86,1040,1023],{"class":107},[86,1042,1043],{"class":147}," 'watch'",[86,1045,771],{"class":107},[86,1047,1048],{"class":92}," input.onlyShorts) ",[86,1050,576],{"class":107},[86,1052,1002],{"class":147},[86,1054,1055],{"class":88,"line":680},[86,1056,591],{"emptyLinePlaceholder":590},[86,1058,1060,1062,1065,1067],{"class":88,"line":1059},14,[86,1061,561],{"class":107},[86,1063,1064],{"class":92}," (input.navigating) ",[86,1066,576],{"class":107},[86,1068,1069],{"class":147}," 'swallow'\n",[86,1071,1073,1075,1078,1080],{"class":88,"line":1072},15,[86,1074,561],{"class":107},[86,1076,1077],{"class":92}," (input.repeat) ",[86,1079,576],{"class":107},[86,1081,1069],{"class":147},[86,1083,1085],{"class":88,"line":1084},16,[86,1086,591],{"emptyLinePlaceholder":590},[86,1088,1090,1092,1095,1097,1100,1103,1106,1109],{"class":88,"line":1089},17,[86,1091,759],{"class":107},[86,1093,1094],{"class":92}," input.pageType ",[86,1096,1023],{"class":107},[86,1098,1099],{"class":147}," 'shorts'",[86,1101,1102],{"class":107}," ?",[86,1104,1105],{"class":147}," 'like-short'",[86,1107,1108],{"class":107}," :",[86,1110,1111],{"class":147}," 'like-watch'\n",[86,1113,1115],{"class":88,"line":1114},18,[86,1116,683],{"class":92},[10,1118,1119,1122,1123,1125],{},[49,1120,1121],{},"swallow"," significa cancelar la tecla sin dar like. Es lo que pasa mientras YouTube está cambiando de vídeo. Antes, en ese momento, la ",[49,1124,820],{}," se escapaba al reproductor y el vídeo avanzaba 10 segundos. También pasa si mantienes la tecla pulsada, que antes daba y quitaba el like en bucle. Al ser una función sin DOM ni eventos, cada caso tiene su test de una línea.",[28,1127,1129],{"id":1128},"lo-que-sigue-pendiente","Lo que sigue pendiente",[10,1131,1132,1133,1136,1137,1140],{},"Los ",[44,1134,1135],{},"tiempos fijos"," siguen ahí: 400 ms de margen después de navegar, 150 ms para comprobar el clic y 250 ms para reintentar. Cambiarlos por un ",[49,1138,1139],{},"MutationObserver"," que espere a que el botón esté listo de verdad es el siguiente paso, pero quería que esta versión no cambiara nada que no tuviera un test detrás.",[10,1142,1143],{},"En los vídeos normales, la última estrategia sigue buscando por texto. Ahora pesa menos: en todas las capturas, la segunda estrategia encuentra el botón antes, también con YouTube en alemán.",[28,1145,1147],{"id":1146},"lo-que-me-enseñó","Lo que me enseñó",[10,1149,1150,1151,1153,1154,26],{},"Lo más útil de esta versión no fueron los tests en sí, sino hacer las capturas. Llevaba meses explicando cómo funcionaba la extensión en los Shorts, y la explicación había dejado de ser cierta sin que nada fallara a la vista. Funcionaba por un plan de respaldo y por la suerte de que ",[36,1152,455],{}," contiene ",[36,1155,447],{},[10,1157,1158],{},"Con una herramienta que hace una sola cosa sobre una web que no controlas, lo que crees que pasa y lo que pasa se separan poco a poco. Una captura real tarda dos minutos y lo dice en seguida.",[10,1160,1161,1162,26],{},"Puedes instalar YouTube Like Shortcut desde la ",[14,1163,1167],{"href":1164,"rel":1165},"https:\u002F\u002Fchromewebstore.google.com\u002Fdetail\u002Fyoutube-like-shortcut\u002Flcpkmnlnllegnfhanjoeeejmnhgpnclm",[1166],"nofollow","Chrome Web Store",[1169,1170,1171],"style",{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sA_wV, html code.shiki .sA_wV{--shiki-default:#032F62;--shiki-dark:#DBEDFF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":82,"searchDepth":114,"depth":114,"links":1173},[1174,1177,1178,1181,1184,1185],{"id":30,"depth":114,"text":31,"children":1175},[1176],{"id":56,"depth":136,"text":57},{"id":416,"depth":114,"text":417},{"id":481,"depth":114,"text":482,"children":1179},[1180],{"id":695,"depth":136,"text":696},{"id":787,"depth":114,"text":788,"children":1182},[1183],{"id":890,"depth":136,"text":891},{"id":1128,"depth":114,"text":1129},{"id":1146,"depth":114,"text":1147},"2026-10-30","La versión 1.5.1 cumple lo que dejé pendiente. Tests que cargan capturas reales de YouTube, el botón de los Shorts encontrado por su componente y no por su texto, y seis fallos que salieron al mirar con lupa. También lo que las capturas revelaron sobre cómo funcionaba la extensión hasta ahora.",false,"md",{},"\u002Fposts\u002Fyoutube-like-shortcut-tests-con-capturas-reales",{"title":5,"description":1187},"posts\u002Fyoutube-like-shortcut-tests-con-capturas-reales",[1195,1196,1197,1198],"youtube-like-shortcut","youtube","testing","typescript","tecnico","8Guf8qGqIeGDoPNxJ7rkR6NFK5hBA_7FpiJ2zi6BkJ8",null,1793520623169]