
CSS
ã€ãã³ã
ãã¬ãžã³
æè¡ããã°
ã¯ããã« ããã«ã¡ã¯ãæ°åäžå¹Žç®ãšã³ãžãã¢ã®è€åã§ãã 倧åŠã§ã¯äž»ã«ã¡ãã£ã¢å·¥åŠãåŠãã§ãããããã°ã©ãã³ã°çµéšã¯ããŸããããŸããã§ãããç ä¿®ã§ã¯Javaãã¡ã€ã³ã«åŠã³ãHTML,CSS,JavaScriptã¯ã»ãã®å°ãè§ŠããçšåºŠã§ãã ãããã8æã«éšçœ²ã«é
å±ãããŠä»¥éã¯ãReact/Next.jsãçšããŠç€Ÿå
ã·ã¹ãã ã®ããã³ããšã³ããéçºããŠããŸãããã®ãããå³ãå·Šãåããããåžžã«ãåãããªãããé ããããæ¯æ¥ã§ãã ç°¡åãªããŒãäœæããå§ãŸããç»é¢ã®ãã¶ã€ã³åã蟌ã¿ãããã¯ãšã³ãã®APIç¹ããã¿ãžãšãæ
åœã¿ã¹ã¯ã®ã¬ãã«ãäžããäžã§ãçŽé¢ããåãããªãããšãžã®è§£æ±ºçãèããŠãããšããåããâŠ
ã¯ããã« ã¯ãããŸããŠã2025幎4æã«æ°åã§ãããã£ã«å
¥ç€Ÿããå®®æã§ãã ãããã£ã®ãšã³ãžãã¢è·ã«ã¯ On-the-Job TrainingïŒOJTïŒ ãšããå¶åºŠããããç§ãå
¥ç€Ÿãã幎床ã§ã¯å
¥ç€ŸåŸã®çŽ1幎éã§ 3ã€ã®ç°ãªãããŒã ãçµéšããŸãããåæçŽ3ã¶æãå®éã®æ¥åã«æºãããªããæè¡ãšããŒã ã¯ãŒã¯ãåŠãã§ããä»çµã¿ã§ãã ãã®èšäºã§ã¯ãç§ãOJTã§çµéšãã3ã€ã®ããŒã ã§ã®æ¥åå
容ããããããã»èŠåŽããç¹ããšããœãŒãããŒã¹ã§ãäŒãããŸãã ããããã£ã®OJTã£ãŠå®éã©ããªæããªã®ïŒã ãšããçåãæã€æ¹ã«ãå°ãã§ããªã¢ã«ãªé°å²æ°ãäŒããã°å¬ããã§ãã OJTã®åã«ïŒæ°äººç ä¿®ïŒ4æã6æïŒ OJTãå§ãŸãåã«ãçŽ2ã¶æéã® æ°äººç ä¿® ããããŸããç ä¿®ã¯å€§ãã3ã€ã®ãã§ãŒãºã«åãããŠããŸãã å
±éç ä¿®ïŒ4æïŒ ãŸãã¯ããžãã¹è·ãšååã®å
±éç ä¿®ããã¹ã¿ãŒãããŸãã瀟äŒäººãšããŠã®åºç€ããããã£ã®äºæ¥ã«ã€ããŠåŠã¶æéã§ããè·çš®ãåããåæå
šå¡ã§åããããããšã³ãžãã¢ä»¥å€ã®åæãšãé¢ä¿ãç¯ãã貎éãªæ©äŒã§ããã æè¡ç ä¿®ïŒ5æïŒ ãšã³ãžãã¢è·ã®ã¿ã®æè¡ç ä¿®ã«ç§»ããŸããå€éšã®ç ä¿®äŒç€Ÿã«ããè¬çŸ©åœ¢åŒã§ãWebéçºã®åºç€ãäœç³»çã«åŠã³ãŸããHTML/CSS/JavaScriptãšãã£ãããã³ããšã³ãã®åºç€ãããLinuxããµãŒãã®åºæ¬ãããã¯ãšã³ãæè¡ãã³ã³ããããã¹ãææ³ãŸã§å¹
åºãã«ããŒãããŸããããã°ã©ãã³ã°çµéšã®å·®ã«é¢ããããå
šå¡ãåãã¹ã¿ãŒãã©ã€ã³ã«ç«ãŠãããèšèšãããŠããŸããã ãšã³ãžãã¢å®äŸïŒ6æé ïŒ æåŸã«ã 瀟å
ã®ãšã³ãžãã¢ãè¬åž«ãåããç ä¿® ïŒãšã³ãžãã¢å®äŸïŒãåããŸããGitåºç€ãAWSåºç€ãçæAIãªã©ããããã£ã®çŸå Žã§å®éã«äœ¿ãããŠããæè¡ãææ³ãå
茩ãšã³ãžãã¢ããçŽæ¥åŠã¹ãŸããå€éšç ä¿®ã§åŸãåºç€ç¥èãããããã£ã®å®åã«ã©ã掻ããããšããèŠç¹ã§æ·±æãããŠããå
容ã§ãOJTã«å
¥ãåã®ç·ä»äžããšãªããŸããã OJTã®åºæ¬çãªæµã åæã®æµãã¯ããããå
±éããŠããŸãã ç®æšèšå® â é
å±åŸããã¬ãŒããŒãäžé·ãšé¢è«ãããã®æã§éæãããç®æšã決ããŸã éçºæ¥å â ã¹ã¯ã©ã ãªã©ã®éçºææ³ãéããŠãå®éã®ãããã¯ãéçºã«åå ããŸã æ¯ãè¿ã â æã®çµããã«ç®æšã®éæåºŠãæ¯ãè¿ããæ¬¡æãžã®åŒãç¶ããè¡ããŸã ãã¬ãŒããŒã®æ¹ãæ¥ã
ã®çžè«çžæãšããŠã€ããŠãã ããã®ã§ãå°ã£ããšãã«ããèããç°å¢ãæŽã£ãŠããŸãã äžæïŒSSOããŒã ïŒ6/12ã9/20ïŒ ããŒã ã«ã€ã㊠ãµãŒãã¹ã€ã³ãã©ããŒã ã® ããããŽãµãããŒã ã«é
å±ãããŸãããååããæ³åãèŸããããããŸãããããããã£å
šäœã§äœ¿ãããŠããèªèšŒã»èªå¯ã·ã¹ãã ãSSOã ããã®ä»ãŠãŒã¶ãŒç®¡çã«äœ¿çšãããã·ã¹ãã ãéçºã»éçšããããŒã ã§ãã SSOã¯å
šç€Ÿå
±éã®åºç€ã§ãããæ¢ãŸããšãããã£ã®å€ãã®ãµãŒãã¹ã«åœ±é¿ãåºãŸãããã®ããã倿Žã«ã¯æ
éããæ±ããããç°å¢ã§ããã äœ¿çšæè¡ OpenID ConnectïŒOIDCïŒ PythonïŒDjangoïŒ AWS äž»ãªæ¥åå
容 SSOãOIDCã®æšæºä»æ§ã«è¿ã¥ããæ¹ä¿® @nifty åªåŸ
ãµãŒãã¹ã®è²©è·¯æ¡å€§ã«åããæ¹ä¿® æ°èŠãµãŒãã¹ã«OIDCå°å
¥ããããã®æ¹ä¿® ãããã æããããããæããã®ã¯ã SSOãOpenID ConnectïŒOIDCïŒã®æšæºä»æ§ã«è¿ã¥ããæ¹ä¿® ãè¡ããããšã§ãã å®ã¯å€§åп代ã«OIDCãæŽ»çšããç ç©¶ãããŠããããããã®åéã«ã¯éŠŽæã¿ããããŸãããã¹ããªã³ãã®ãã©ã³ãã³ã°ã§æšæºåã®æ¹éãæ€èšãããŠããéãéçºãé²ããäžã§ããã®ä¿®æ£ã ãã§ã¯è¶³ããªãç®æããããããšã«æ°ã¥ããèªã远å ã®ä¿®æ£ãææ¡ã»å®è£
ããŸããã çµæãšããŠãOIDCã®ã©ã€ãã©ãªãå°å
¥ããã ãã§SSOã䜿ããããã«ãªãããããã¯ãã®å©äŸ¿æ§åäžã«è²¢ç®ã§ããŸããã åŠçæä»£ã«åŠãã ããšãå®åã§æŽ»ããç¬é ã¯æ¬åœã«å¬ããã£ãã§ãã èŠåŽããç¹ äžæ¹ã§ãåããŠã®é
å±ãšããããšãããã å®åã®éçºããã»ã¹ãäžããç¿åŸãã å¿
èŠããããŸãããã¹ã¯ã©ã ããã¹ãã³ãŒãã®æžãæ¹ãæ¬çªç°å¢ãæ¢ããã«å質ãæ
ä¿ããããã®å·¥å€«ãªã©ãåŠçæä»£ã«ã¯çµéšã®ãªãã£ãããšã°ããã§æåã¯èŠæŠããŸããã ãŸããé害察å¿ã®å Žé¢ã«ç«ã¡äŒã£ãéã«èªåã®ç¡åããçæããããšããããŸããåœæã¯ãäžäººã§è§£æ±ºããªããã°ããšããæèã匷ããé£ããã¿ã¹ã¯ãæ±ã蟌ã¿ãããŠããŸãããšããããŸãããããã¯åŸã®æã§å€§ããªåŠã³ã«ç¹ãããŸãã äºæïŒããŒã¿ã«ããŒã ïŒ9/21ã12/20ïŒ ããŒã ã«ã€ã㊠第äžéçºããŒã ã® @niftyãããããŒãžãæ
åœãããµãããŒã ã«é
å±ãããŸããã @niftyãããããŒãž ã®éçºã»éçšã«å ããæ°èŠäºæ¥ã®éçºãæãããããŒã ã§ãã ãµãŒãã¹ã®äŒç»æ
åœè
ãšå¯ã«ã³ãã¥ãã±ãŒã·ã§ã³ãåããªããé²ããã¹ã¿ã€ã«ã§ãAWSãã¯ãããšããã¢ãã³ãªæè¡ã¹ã¿ãã¯ã䜿ã£ãéçºãç¹åŸŽã§ããã¹ã¯ã©ã éçºïŒ2é±éã¹ããªã³ãïŒãæ¡çšãããã€ãªãŒã¹ã¯ã©ã ã§ããŒã å
šäœã®é²æãå
±æããŠããŸããã äœ¿çšæè¡ microCMS Next.js AWSïŒTerraformïŒ äž»ãªæ¥åå
容 ã@niftyãããããŒãžãã®éçºã»éçš æ°èŠäºæ¥ã®AWSã€ã³ãã©ããã³ããã³ããšã³ãã®æ§ç¯ ããããã»èŠåŽããç¹ ãã®æã§æããããããšèŠåŽãæããã®ã¯ã æªçµéšã®ç¶æ
ããAWSç°å¢ã®æ§ç¯ã«ãŒãããææŠããããš ã§ãã åœåã¯ããã³ããšã³ãå¯ãã®æ¥åãæ³å®ããŠããŸããããããŒã ã®ç¶æ³ãèªåèªèº«ã®åžæããã Terraformæªçµéšã®ç¶æ
ããã€ã³ãã©æ§ç¯ ãä»»ãããããšã«ãªããŸãããæ£çŽãæåã¯äžå®ããããŸãããããããçµæçã«å€§ããªæé·ã®ãã£ããã«ãªããŸãã å
茩æ¹ãéå»ã«æ§ç¯ããç°å¢ãåèã«ããªãããVPCã»DNSã»CI/CDãã€ãã©ã€ã³ã®äœæãé²ããŸããããã ãä»åã®èŠä»¶ãšéå»ã®æ§æã«ã¯å·®åããããåããŠè§Šãæè¡ã®äžã§ãã®å·®åãåããäœæ¥ã«ã¯éåžžã«èŠåŽããŸããã äžæã§ã®åçãæŽ»ããã å°ã£ããšãã¯äžäººã§æ±ã蟌ãŸããç©æ¥µçã«ããŒã ãžçžè«ãã ããšãæèããŸãããå
茩æ¹ã®æåããã©ããŒã®ãããã§ãæçµçã«ã¯æ°èŠäºæ¥ã®ããã³ããšã³ãã€ã³ãã©ãã»ãŒäžäººã§æ§ç¯ããåäœç¢ºèªãŸã§å®äºã§ããŸããã ãŸããäžæã§SSOïŒèªèšŒïŒã®çµéšããã£ãããšãäºæã§æŽ»ããæ°èŠäºæ¥ã®SSOå°å
¥æã«ç確ãªå©èšãã§ããããšãå°è±¡ã«æ®ã£ãŠããŸãã ç°ãªãããŒã ã§ã®çµéšãç¹ããç¬é ã¯ãOJTãªãã§ã¯ã ãšæããŸããã äžæïŒéåããŒã ïŒ12/21ã3/20ïŒ ããŒã ã«ã€ã㊠å
¥äŒã·ã¹ãã ããŒã ã® éåãµãããŒã ã«é
å±ãããŸããã代çåºæ§ãå
åç·ããªãã·ã§ã³ãµãŒãã¹ã®å
¥äŒã«äœ¿çšããã·ã¹ãã ã®ãéçºã»éçšãæ
åœããŠããŸãã 代çåºæ§ãšã®æ¥ç¹ãå€ãã瀟å
å€ã®è€æ°éšçœ²ãšã®é£æºãæ±ããããç°å¢ã§ãã äœ¿çšæè¡ PHPïŒCakePHPïŒ AWSïŒTerraformïŒ äž»ãªæ¥åå
容 ã@nifty ã€ãªãã¢ãã€ã«ããããå€ãã®æ¹ã«ç³ã蟌ãããããç²åŸä»£çåºæ§åãã®ç³èŸŒããŒã«ãæ¹ä¿® ãµã€ã³ã¢ããã·ã¹ãã ã«ã¯ã¬ã«ãã§ãã¯æ©èœã远å å
ã³ã©ãå
¥äŒæã®ãªãã·ã§ã³ãµãŒãã¹ç³ã蟌ã¿ãã©ãŒã ã®ãã¶ã€ã³å·æ° ããããã£äŒå¡ç¹å¥ã§ãããã©ã³ãç³ã蟌ã¿ã·ã¹ãã ã®AWSç§»è¡ïŒTerraformïŒ ããããã»èŠåŽããç¹ äžæã§ã¯ è€æ°ã®ãããã¯ããæšªæããŠæ¹ä¿®ãã ãšããããããŸã§ã«ãªãçµéšãããŸããã äžã§ããªãã·ã§ã³ãµãŒãã¹ç³ã蟌ã¿ãã©ãŒã ã®ãã¶ã€ã³å·æ°ã¯ããµãŒãã¹ã®äŒç»ãå¶äœãè¡ãããŒã ãå
ã³ã©ãç³ã蟌ã¿ã·ã¹ãã ã®ããŒã çãšé£æºããªããé²è¡ãããããžã§ã¯ãã§ãåããŠçµéšãã 倿¹é¢ãšã®ãã«ããã¹ãªèª¿æŽ ã«é£ãããæããŸããããã ãäžæã»äºæã§å¹ã£ããçžè«ããããšããåšå²ãå·»ã蟌ãããšãã®å€§åããå®è·µã§ããŠããæå¿ãããããŸãã ãŸããã§ããã·ã¹ãã ã®AWSç§»è¡ã§ã¯ãäžæã®AWSçµéšãšäºæã®Terraformçµéšããã®ãŸãŸæŽ»ããŸãããOJTãéããŠç©ã¿äžããŠããæè¡ã äžæã§çµ±åããã ããã«æããèªåèªèº«ã®æé·ã宿ã§ããç¬éã§ããã OJTå
šäœãéã㊠3ããŒã ã®å
±éç¹ ã¹ã¯ã©ã éçº â äžéšçœ²ãšãã¹ã¯ã©ã ãæ¡çšããŠãããäžåºŠèº«ã«ã€ããéçºã®åã¯ã©ã®ããŒã ã§ã掻ããŸãã AIã®æŽ»çšæšé² â äžæã§ã¯AIæšé²ããŒã ã®ã¡ã³ããŒããããäºæã§ã¯Kiroã䜿ã£ãspecéçºã宿œããããªã©ãåããŒã ã§ç©æ¥µçã«AIãåãå
¥ããŠããŸã äººã®æž©ãã â ã©ã®ããŒã ã§ããããããªãããšãããã°æéãå²ããŠäžå¯§ã«æããŠãã ããå
茩æ¹ã°ããã§ãã æè¡ã®å¹
ã®åºãã OJTãéããŠè§Šããæè¡ã¯ãæããšã«å€§ããç°ãªããŸãã äžæ : Python / Django / OIDC äºæ : Next.js / microCMS / Terraform äžæ : PHP / CakePHP / Terraform ããã¯ãšã³ã â ããã³ãïŒã€ã³ãã© â ã¬ã¬ã·ãŒïŒã¢ãã³ã®æ··åšç°å¢ãšã æ¯æãŸã£ããç°ãªãæè¡ã¹ã¿ãã¯ã«è§Šãããã ã®ã¯OJTã®å€§ããªé
åã§ãã äžçªã®æé·ïŒãçžè«ããåã æ¯ãè¿ããšãOJTãéããäžçªã®æé·ã¯ æè¡åããããçžè«ããåã ãããããŸããã äžæã§ã¯ãäžäººã§ããããšããããŠããããšããåçããããŸãããäºæã§ã¯ãããæèçã«æ¹åããç©æ¥µçã«ããŒã ãžçžè«ããããã«ããŸããããããŠäžæã§ã¯ããã¬ãŒããŒãããèªèµ°ã§ããããšè©äŸ¡ããããŸã§ã«ãªããŸããã äžäººã§æ±ã蟌ãŸãªãããš ã¯ãæè¡ãåŠã¶ããšãšåãããã倧åãªã¹ãã«ã ãšå®æããŠããŸãã ããããæé·ãå®çŸã§ããã®ã¯ã 3ã€ã®ããŒã ãæž¡ãæ©ããªãããæ¯åæ°ããç°å¢ã§ãçžè«ãããçµéšãç©ã¿éãããããããã£ã®OJTã ãããã ã ãšåŒ·ãæããŠããŸããããŒã ãå€ãããã³ã«é¢ä¿æ§ç¯ããããçŽã倧å€ãã¯ãããŸããããã®ç¹°ãè¿ãããããçžè«ããåããæ¬ç©ã«ããŠãããŸããã ãããã£ã«èå³ãæã£ãŠãããæ¹ãž ç§ãå
¥ç€Ÿãã幎床ã§ã¯ãOJTãéããŠçŽ1幎éã§ 3ã€ã®ç°ãªãããŒã ãçµéšããŸãããæ¯åæ°ããç°å¢ã»æ°ããæè¡ã»æ°ãã人éé¢ä¿ã«ãŒãããé£ã³èŸŒãã®ã¯æ£çŽå€§å€ã§ããããã ãããã å§åçã«èŠéãåºããã確ããªæé·ã宿ã§ãã 1幎éã§ããã ç§ãäŒãããããšã¯3ã€ã§ãã åŠçæä»£ã®çµéšã¯æŽ»ããå Žåããã â ç§ã®å Žåã¯OIDCã®ç ç©¶çµéšãäžæã§å³æŠåã«ãªããŸãããã©ããªçµéšãç¡é§ã«ã¯ãªããŸãã å°ã£ããçžè«ããŠã»ãã â ãããã£ã«ã¯åªããå
茩æ¹ãããããããŸããäžäººã§æ±ã蟌ãå¿
èŠã¯ãããŸãã æ¯æãæ°ãããã£ã¬ã³ãž â ãã§ããªããããã§ãããã«å€ããç¬éããOJTã§ã¯äœåºŠãå³ãããŸã ãã®èšäºãããããã£ã«èå³ãæã£ãŠãããæ¹ã«ãšã£ãŠãå°ãã§ãåãã€ã¡ãŒãžãæã€ãã£ããã«ãªãã°å¹žãã§ãã
PSSLã®äœã
æšã§ãã E2Eãã¹ãã¯éèŠã ãšããã£ãŠããŠããPlaywrightã®ã³ãŒããæžãã®ãé¢åã§åŸåãã«ããŠããŸãããïŒ æ¬èšäºã§ã¯ã Markdownãã¡ã€ã«ã«æ¥æ¬èªã§æäœæé ãæžãã ãã§ãPlaywrightãèªåå®è¡ããŠãããE2Eãã¹ããã¬ãŒã ã¯ãŒã¯ ã®äœãæ¹ããå®éã®ãããã¯ã·ã§ã³äºäŸãããšã«è§£èª¬ããŸãã ãã®èšäºã§ãããããš Markdownã·ããªãªé§åã®E2Eãã¹ãã®å
šäœã¢ãŒããã¯ã㣠èªç¶èšèªã¹ããããPlaywrightã¢ã¯ã·ã§ã³ã«å€æããä»çµã¿ ãã©ãŒã å
¥åã®å€æ®µãã©ãŒã«ããã¯æŠç¥ åç»èšé²ã»ã¹ã¯ãªãŒã³ã·ã§ããã«ãããããã°æ¯æŽ pre-commitããã¯ãšã®é£æºã«ããéçºãããŒçµ±å ãªãMarkdownã§E2Eãã¹ããæžãã®ã åŸæ¥ã®E2Eãã¹ãã«ã¯3ã€ã®åé¡ããããŸããã ãã¹ãã³ãŒãã仿§ãšä¹é¢ãã â Playwrightã®ã³ãŒããèªãã§ããäœã®ã·ããªãªããã¹ãããŠããã®ãã²ãšç®ã§ããããªã éãšã³ãžãã¢ãã¬ãã¥ãŒã§ããªã â PMããã¶ã€ããŒããã¹ãã±ãŒã¹ã確èªã»è¿œå ã§ããªã ã¡ã³ããã³ã¹ã³ã¹ããé«ã â ã»ã¬ã¯ã¿ã®å€æŽã²ãšã€ã§å€§éã®ãã¹ããå£ãã Markdownã·ããªãªé§åãã¹ããªããããæžããŸãïŒ ## ã·ããªãª: 管çè
ãã°ã€ã³æå 1. <http://localhost:8055/login/> ã«ã¢ã¯ã»ã¹ãã 2. ã¡ãŒã«ã¢ãã¬ã¹ã«ãadmin@example.comããå
¥åãã 3. ãã¹ã¯ãŒãã«ãadmin123ããå
¥åãã 4. ããã°ã€ã³ããã¿ã³ãã¯ãªãã¯ãã 5. ãããã·ã¥ããŒãããšããããã¹ããç»é¢ã«è¡šç€ºãããŠããããšã確èªãã 6. URLã«ã/admin/dashboardããå«ãŸããããšã確èªãã 誰ãèªãã§ãäœããã¹ãããŠãããããããŸãã å
šäœã¢ãŒããã¯ã㣠ãã¬ãŒã ã¯ãŒã¯ã¯4ã€ã®ã³ã³ããŒãã³ãã§æ§æãããŸãã Markdownã·ããªãªãã¡ã€ã« (.md) | [Parser] Markdownãæ§é åããŒã¿ã«å€æ | [Step Mapper] èªç¶èšèª â Playwrightã¢ã¯ã·ã§ã³ | [Runner] ãã©ãŠã¶æäœã®å®è¡ã»åç»èšé²ã»ã¬ããŒã åã³ã³ããŒãã³ãã®ã³ãŒãéã¯é©ãã»ã©å°ãããParserçŽ90è¡ãStep MapperçŽ280è¡ãRunnerçŽ280è¡ã§å®çŸã§ããŸãã ãããã解説ããŠãããŸãã Step 1: Markdownã·ããªãªã®ãã©ãŒããããå®çŸ©ãã ãŸãããã¹ãã·ããªãªãèšè¿°ããMarkdownã®ãã©ãŒããããæ±ºããŸãã # èªèšŒæ©èœãã¹ã ## åææ¡ä»¶ - ãã¹ãçšç®¡çè
ãååšããïŒemail: admin@example.com, password: admin123ïŒ - ãã¹ãçšä»£çåºãŠãŒã¶ãŒãååšããïŒemail: user@example.com, password: user123ïŒ ## ã·ããªãª: 管çè
ãã°ã€ã³æå â ãã°ã¢ãŠã 1. <http://localhost:8055/login/> ã«ã¢ã¯ã»ã¹ãã 2. ã¡ãŒã«ã¢ãã¬ã¹ã«ãadmin@example.comããå
¥åãã 3. ãã¹ã¯ãŒãã«ãadmin123ããå
¥åãã 4. ããã°ã€ã³ããã¿ã³ãã¯ãªãã¯ãã 5. ãããã·ã¥ããŒãããšããããã¹ããç»é¢ã«è¡šç€ºãããŠããããšã確èªãã 6. URLã«ã/admin/dashboardããå«ãŸããããšã確èªãã 7. ãã°ã¢ãŠããã ## ã·ããªãª: ãã¹ã¯ãŒãééã 1. <http://localhost:8055/login/> ã«ã¢ã¯ã»ã¹ãã 2. ã¡ãŒã«ã¢ãã¬ã¹ã«ãadmin@example.comããå
¥åãã 3. ãã¹ã¯ãŒãã«ãwrongpasswordããå
¥åãã 4. ããã°ã€ã³ããã¿ã³ãã¯ãªãã¯ãã 5. ãã¡ãŒã«ã¢ãã¬ã¹ãŸãã¯ãã¹ã¯ãŒããæ£ãããããŸããããšããããã¹ãã衚瀺ãããããšã確èªãã ã«ãŒã«ã¯ã·ã³ãã«ã§ãïŒ èŠçŽ èšæ³ äŸ ãã¹ããã¡ã€ã«ã®ã¿ã€ãã« # ã¿ã€ãã« # èªèšŒæ©èœãã¹ã åææ¡ä»¶ ## åææ¡ä»¶ + ç®æ¡æžã - ãã¹ãçšãŠãŒã¶ãŒãååšãã ã·ããªãª ## ã·ããªãª: åå + çªå·ãªã¹ã ## ã·ããªãª: ãã°ã€ã³æå ã¹ããã 1. æäœå
容 1. ããã°ã€ã³ããã¿ã³ãã¯ãªãã¯ãã 1ãã¡ã€ã«ã«è€æ°ã·ããªãªãæžããŸããåææ¡ä»¶ã»ã¯ã·ã§ã³ã¯ããã¥ã¡ã³ããšããŠæ©èœããã·ãŒãããŒã¿ã®ä»æ§ãæç€ºãã圹å²ãæãããŸãã Step 2: MarkdownããŒãµãŒãå®è£
ãã Markdownãã¡ã€ã«ãè§£æããŠæ§é åããŒã¿ã«å€æããããŒãµãŒãäœããŸãã # parser.py import re from dataclasses import dataclass, field from pathlib import Path @dataclass class Scenario: name: str steps: list[str] = field(default_factory=list) @dataclass class ScenarioFile: path: Path title: str preconditions: list[str] = field(default_factory=list) scenarios: list[Scenario] = field(default_factory=list) def parse_scenario_file(filepath: Path) -> ScenarioFile: """Markdownã·ããªãªãã¡ã€ã«ãè§£æãã""" text = filepath.read_text(encoding="utf-8") lines = text.splitlines() title = "" preconditions: list[str] = [] scenarios: list[Scenario] = [] current_section = None current_scenario: Scenario | None = None for raw_line in lines: line = raw_line.strip() # ãããã¬ãã«ã¿ã€ãã« if line.startswith("# ") and not line.startswith("## "): title = line[2:].strip() continue # ã»ã¯ã·ã§ã³ããã㌠if line.startswith("## "): header = line[3:].strip() if "åææ¡ä»¶" in header: current_section = "preconditions" current_scenario = None continue # ã·ããªãªæ€åº m = re.match(r"^ã·ããªãª[:ïŒ]\\s*(.+)", header) if m: current_scenario = Scenario(name=m.group(1).strip()) scenarios.append(current_scenario) current_section = "scenario" continue # ç®æ¡æžãïŒåææ¡ä»¶ïŒ m_bullet = re.match(r"^[-*]\\s+(.+)", line) if m_bullet: content = m_bullet.group(1).strip() if current_section == "preconditions": preconditions.append(content) elif current_section == "scenario" and current_scenario: current_scenario.steps.append(content) continue # çªå·ä»ããªã¹ãïŒã·ããªãªã¹ãããïŒ m_num = re.match(r"^\\d+\\.\\s+(.+)", line) if m_num and current_section == "scenario" and current_scenario: current_scenario.steps.append(m_num.group(1).strip()) return ScenarioFile( path=filepath, title=title or filepath.stem, preconditions=preconditions, scenarios=scenarios, ) ãã€ã³ãã¯çªå·ä»ããªã¹ãããçªå·ãã¬ãã£ãã¯ã¹ãé€å»ããŠã¹ãããæååã ããæœåºããŠããããšã§ãã 1. ããã°ã€ã³ããã¿ã³ãã¯ãªãã¯ãã â ããã°ã€ã³ããã¿ã³ãã¯ãªãã¯ãã Step 3: ã¹ãããããããŒãå®è£
ããïŒã³ã¢éšåïŒ ããããã®ãã¬ãŒã ã¯ãŒã¯ã®å¿èéšã§ããèªç¶èšèªã®ã¹ããããæ£èŠè¡šçŸã§ãã¿ãŒã³ããããã察å¿ããPlaywrightã¢ã¯ã·ã§ã³ãå®è¡ããŸãã åºæ¬æ§é # step_mapper.py import re from playwright.sync_api import Page, expect def execute_step(page: Page, step: str, base_url: str, timeout: int = 30000): """èªç¶èšèªã¹ããããè§£éããŠPlaywrightã¢ã¯ã·ã§ã³ãå®è¡ãã""" # --- ããã²ãŒã·ã§ã³ --- m = re.search(r"(https?://\\S+)\\s*(?:ã«|ãž)ã¢ã¯ã»ã¹ãã", step) if m: page.goto(m.group(1), timeout=timeout) return m = re.search(r"(/.+?)\\s*(?:ã«|ãž)(?:ã¢ã¯ã»ã¹|é·ç§»|ç§»å)ãã", step) if m: page.goto(base_url + m.group(1), timeout=timeout) return # ... ä»ã®ãã¿ãŒã³ãç¶ã 察å¿ããã¹ããããã¿ãŒã³äžèЧ ãã¬ãŒã ã¯ãŒã¯ãèªèããã¹ããããã¿ãŒã³ã玹ä»ããŸãã ããã²ãŒã·ã§ã³ # 絶察URL # äŸ: "<http://localhost:8055/login/> ã«ã¢ã¯ã»ã¹ãã" m = re.search(r"(https?://\\S+)\\s*(?:ã«|ãž)ã¢ã¯ã»ã¹ãã", step) if m: page.goto(m.group(1), timeout=timeout) return # çžå¯Ÿãã¹ # äŸ: "/agency/dashboard ã«ã¢ã¯ã»ã¹ãã" m = re.search(r"(/.+?)\\s*(?:ã«|ãž)(?:ã¢ã¯ã»ã¹|é·ç§»|ç§»å)ãã", step) if m: page.goto(base_url + m.group(1), timeout=timeout) return ãªã³ã¯ã»ãã¿ã³ã®ã¯ãªã㯠# ãªã³ã¯ãã¯ãªã㯠# äŸ: ãç©ä»¶ç®¡çããªã³ã¯ãã¯ãªãã¯ãã m = re.search(r"[ãã](.+?)[ãã](?:ãªã³ã¯|ã¡ãã¥ãŒ)ãã¯ãªãã¯ãã", step) if m: text = m.group(1) page.get_by_role("link", name=text).first.click(timeout=timeout) page.wait_for_load_state("networkidle") return # ãã¿ã³ãã¯ãªã㯠# äŸ: ããã°ã€ã³ããã¿ã³ãã¯ãªãã¯ãã m = re.search(r"[ãã](.+?)[ãã]ãã¿ã³ã(?:ã¯ãªãã¯|æŒ)ãã", step) if m: text = m.group(1) # submit ãã¿ã³ãåªå
çã«æ¢ã submit_buttons = page.locator("button[type='submit']:visible") if submit_buttons.count() > 0: for i in range(submit_buttons.count()): btn = submit_buttons.nth(i) if text in (btn.inner_text() or ""): btn.click(timeout=timeout) page.wait_for_load_state("networkidle") return # ããã¹ãå®å
šäžèŽããªããã°æåã®submitãã¿ã³ submit_buttons.first.click(timeout=timeout) else: page.get_by_role("button", name=text).first.click(timeout=timeout) page.wait_for_load_state("networkidle") return # ããã¹ãèŠçŽ ãã¯ãªãã¯ïŒããŒãã«è¡ãªã©ïŒ # äŸ: ãSKR-001ãããã¹ããã¯ãªãã¯ãã m = re.search(r"[ãã](.+?)[ãã](?:ããã¹ã|æå|é
ç®|è¡)ãã¯ãªãã¯ãã", step) if m: page.get_by_text(m.group(1), exact=False).first.click(timeout=timeout) return ãã©ãŒã å
¥å # ããã¹ãå
¥å # äŸ: ã¡ãŒã«ã¢ãã¬ã¹ã«ãadmin@example.comããå
¥åãã m = re.search( r"[ãã]?(.+?)[ãã]?(?:æ¬|ãã£ãŒã«ã)?ã«\\s*[ãã](.+?)[ãã]\\s*(?:ãå
¥å|ãšå
¥å)ãã", step ) if m: label, value = m.group(1), m.group(2) _fill_by_label(page, label, value, timeout) return # ã»ã¬ã¯ãããã¯ã¹ # äŸ: ãã¹ããŒã¿ã¹ãã§ãéé»äžããéžæãã m = re.search(r"[ãã](.+?)[ãã](?:ã§|ãã)\\s*[ãã](.+?)[ãã]\\s*ãéžæãã", step) if m: label, value = m.group(1), m.group(2) page.get_by_label(label).select_option(label=value, timeout=timeout) return # æ¥ä»å
¥å # äŸ: åžææ¥ã«ã2026-12-01ããå
¥åãã m = re.search( r"[ãã](.+?)[ãã](?:æ¬|ãã£ãŒã«ã)?ã«\\s*(\\d{4}[-/]\\d{1,2}[-/]\\d{1,2})\\s*ã(?:å
¥å|èšå®)ãã", step ) if m: label = m.group(1) date_str = m.group(2).replace("/", "-") page.get_by_label(label).fill(date_str, timeout=timeout) return ã¢ãµãŒã·ã§ã³ïŒæ€èšŒïŒ # ããã¹ãã衚瀺ãããŠããããšãç¢ºèª # äŸ: ãããã·ã¥ããŒãããšããããã¹ããç»é¢ã«è¡šç€ºãããŠããããšã確èªãã m = re.search( r"[ãã](.+?)[ãã].*(?:衚瀺ãããŠãã|衚瀺ããã|èŠãã|確èªãã|å«ãŸãã|ãã)", step ) if m: text = m.group(1) locator = page.locator( f":not(option):not(select):visible:has-text('{text}')" ).first expect(locator).to_be_visible(timeout=timeout) return # URLã®ç¢ºèª # äŸ: URLã«ã/admin/dashboardããå«ãŸããããšã確èªãã m = re.search(r"URLã«\\s*[ãã](.+?)[ãã]\\s*ãå«ãŸãã", step) if m: expect(page).to_have_url(re.compile(re.escape(m.group(1))), timeout=timeout) return # ããã¹ãã衚瀺ãããŠããªãããšãç¢ºèª # äŸ: ããšã©ãŒããšããããã¹ãã衚瀺ãããŠããªãããšã確èªãã m = re.search(r"[ãã](.+?)[ãã].*(?:衚瀺ãããŠããªã|衚瀺ãããªã|èŠããªã)", step) if m: expect( page.get_by_text(m.group(1), exact=False).first ).not_to_be_visible(timeout=timeout) return ãã®ä» # ãã°ã¢ãŠã if re.search(r"ãã°ã¢ãŠããã", step): page.context.clear_cookies() page.goto(base_url + "/login/") page.wait_for_load_state("networkidle") return # åŸ
æ© # äŸ: 3ç§åŸ
〠m = re.search(r"(\\d+)ç§(?:åŸ
ã€|åŸ
æ©ãã)", step) if m: page.wait_for_timeout(int(m.group(1)) * 1000) return # ãããããªãã£ãå Žå raise ValueError(f"æªå¯Ÿå¿ã®ã¹ããã:{step}") ãã©ãŒã å
¥åã®å€æ®µãã©ãŒã«ããã¯æŠç¥ ãã©ãŒã å
¥åã¯æããããããããã€ã³ãã§ããå®éã®HTML㯠<label> ããªãå Žåã placeholder ã§ä»£çšããŠããå ŽåãCSSãã¬ãŒã ã¯ãŒã¯ç¹æã®ããŒã¯ã¢ãããªã©ã倿§ã§ãã ããã§ã 5段éã®ãã©ãŒã«ããã¯æŠç¥ ãå®è£
ããŸãã def _fill_by_label(page: Page, label: str, value: str, timeout: int): """倿®µãã©ãŒã«ããã¯ã§ãã©ãŒã ãã£ãŒã«ããç¹å®ããŠå
¥åãã""" # Level 1: aria-label / <label> ã«ããç¹å® try: loc = page.get_by_label(label, exact=False).locator("visible=true") if loc.count() > 0: loc.first.fill(value, timeout=timeout) return except Exception: pass # Level 2: placeholder ã«ããç¹å® try: loc = page.get_by_placeholder(label, exact=False).locator("visible=true") if loc.count() > 0: loc.first.fill(value, timeout=timeout) return except Exception: pass # Level 3: labelèŠçŽ ã®DOMæ§é ãã蟿ã try: labels = page.locator(f"label:visible:has-text('{label}')") for i in range(labels.count()): label_elem = labels.nth(i) parent = label_elem.locator("..") inp = parent.locator("input:visible, textarea:visible, select:visible") if inp.count() > 0: inp.first.fill(value, timeout=timeout) return except Exception: pass # Level 4: name屿§ã«ããç¹å®ïŒæ¥æ¬èªã©ãã« â HTMLã®name屿§ãããã³ã°ïŒ field_map = { "ã¡ãŒã«ã¢ãã¬ã¹": "email", "ãã¹ã¯ãŒã": "password", "ç©ä»¶å": "name", "éµäŸ¿çªå·": "postal_code", "éœéåºç": "prefecture", "åžåºçºæ": "city", "çºåçªå°": "address", "建ç©å": "building_name", "éšå±çªå·": "room_number", "管çã³ãŒã": "external_key", "ã¡ã¢": "memo", # å¿
èŠã«å¿ããŠè¿œå } name_attr = field_map.get(label) if name_attr: try: loc = page.locator(f"input[name='{name_attr}']:visible, textarea[name='{name_attr}']:visible") if loc.count() > 0: loc.first.fill(value, timeout=timeout) return except Exception: pass # Level 5: type屿§ã«ããç¹å® type_map = {"ã¡ãŒã«ã¢ãã¬ã¹": "email", "ãã¹ã¯ãŒã": "password"} type_attr = type_map.get(label) if type_attr: try: loc = page.locator(f"input[type='{type_attr}']:visible") if loc.count() > 0: loc.first.fill(value, timeout=timeout) return except Exception: pass raise ValueError(f"å
¥åãã£ãŒã«ããèŠã€ãããŸãã:{label}") ãã®å€æ®µãã©ãŒã«ããã¯ã«ãããã»ãšãã©ã®HTMLãã©ãŒã ã«å¯Ÿå¿ã§ããŸãã ã¬ãã« æ¹æ³ 察å¿ããã±ãŒã¹ 1 get_by_label æ£ãã <label> ãããŒã¯ã¢ããããããã©ãŒã 2 get_by_placeholder placeholder 屿§ã§å
¥åãã³ããæã€ãã©ãŒã 3 DOMæ§é ã蟿ã <label> ãinputãšåã芪èŠçŽ å
ã«ãããã©ãŒã 4 name 屿§ãããã³ã° <label> ããªããname屿§ã¯äžè²«ããŠãããã©ãŒã 5 type 屿§ email/passwordãªã©åã§äžæã«ç¹å®ã§ãããã£ãŒã«ã Step 4: ãã¹ãã©ã³ããŒãå®è£
ãã ã·ããªãªãã¡ã€ã«ã®è§£æãšã¹ãããå®è¡ãçµ±åããã©ã³ããŒãäœããŸãã # runner.py import signal import socket import subprocess import sys import time from dataclasses import dataclass from pathlib import Path from playwright.sync_api import sync_playwright from config import APP_DIR, BASE_URL, BROWSER, HEADLESS, SCENARIOS_DIR, TIMEOUT, VIDEO_DIR from parser import parse_scenario_file from step_mapper import execute_step @dataclass class TestResult: scenario_file: str scenario_name: str passed: bool error: str = "" video_path: str = "" screenshot_path: str = "" def start_django_server(port: int = 8055): """DjangoãµãŒããŒãèµ·åããïŒæ¢ã«èµ·åäžãªãã¹ãããïŒ""" sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: sock.connect(("localhost", port)) sock.close() print(f"ããŒã{port} ã¯æ¢ã«äœ¿çšäžã§ããæ¢åã®ãµãŒããŒã䜿çšããŸãã") return None except ConnectionRefusedError: sock.close() proc = subprocess.Popen( [sys.executable, str(APP_DIR / "manage.py"), "runserver", str(port), "--noreload"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) # ãµãŒããŒã®èµ·åãåŸ
æ© for _ in range(30): try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(("localhost", port)) s.close() print(f"DjangoéçºãµãŒããŒãããŒã{port} ã§èµ·åããŸãã") return proc except ConnectionRefusedError: time.sleep(1) raise RuntimeError("DjangoãµãŒããŒã®èµ·åãã¿ã€ã ã¢ãŠãããŸãã") def stop_django_server(proc): """DjangoãµãŒããŒã忢ãã""" if proc: proc.send_signal(signal.SIGTERM) try: proc.wait(timeout=5) except subprocess.TimeoutExpired: proc.kill() def ensure_seed_data(): """ãã¹ãçšã·ãŒãããŒã¿ãæå
¥ãã""" subprocess.run( [sys.executable, str(APP_DIR / "manage.py"), "seed", "--reset"], check=True, capture_output=True, ) print("ã·ãŒãããŒã¿ãæå
¥ããŸãã") def run_scenarios(scenario_files: list[Path], base_url: str, timeout: int) -> list[TestResult]: """ã·ããªãªãã¡ã€ã«ãå®è¡ããŠçµæãè¿ã""" results = [] with sync_playwright() as p: browser = getattr(p, BROWSER).launch(headless=HEADLESS) for scenario_path in scenario_files: sf = parse_scenario_file(scenario_path) print(f"\\n{'='*60}") print(f"å®è¡:{sf.title} ({scenario_path.name})") print(f"{'='*60}") # ãã¡ã€ã«åäœã§ãã©ãŠã¶ã³ã³ããã¹ãïŒåç»èšé²ïŒãäœæ video_dir = VIDEO_DIR / scenario_path.stem video_dir.mkdir(parents=True, exist_ok=True) context = browser.new_context( record_video_dir=str(video_dir), record_video_size={"width": 1280, "height": 720}, viewport={"width": 1280, "height": 720}, ) page = context.new_page() for scenario in sf.scenarios: print(f"\\n ã·ããªãª:{scenario.name}") passed = True error_msg = "" screenshot_path = "" for i, step_text in enumerate(scenario.steps, 1): try: print(f" ã¹ããã{i}:{step_text} ... ", end="", flush=True) execute_step(page, step_text, base_url, timeout) print("OK") except Exception as e: print(f"FAILED:{e}") passed = False error_msg = f"ã¹ããã{i}:{step_text} ->{e}" # 倱ææã®ã¹ã¯ãªãŒã³ã·ã§ãã ss_path = video_dir / f"{scenario.name}_fail.png" try: page.screenshot(path=str(ss_path)) screenshot_path = str(ss_path) except Exception: pass break results.append(TestResult( scenario_file=scenario_path.name, scenario_name=scenario.name, passed=passed, error=error_msg, screenshot_path=screenshot_path, )) # ã³ã³ããã¹ããéããŠåç»ãç¢ºå® video_path_raw = page.video.path if page.video else None page.close() context.close() # åç»ããªããŒã if video_path_raw: final_video = video_dir / f"{scenario_path.stem}.webm" try: Path(video_path_raw).rename(final_video) for r in results: if r.scenario_file == scenario_path.name: r.video_path = str(final_video) except Exception: pass browser.close() return results def print_summary(results: list[TestResult]): """ãã¹ãçµæã®ãµããªãŒã衚瀺ãã""" print(f"\\n{'='*60}") print("ãã¹ãçµæãµããªãŒ") print(f"{'='*60}") for r in results: icon = "PASS" if r.passed else "FAIL" print(f" [{icon}]{r.scenario_file} >{r.scenario_name}") if r.video_path: print(f" åç»:{r.video_path}") if r.error: print(f" ãšã©ãŒ:{r.error}") if r.screenshot_path: print(f" ã¹ã¯ãªãŒã³ã·ã§ãã:{r.screenshot_path}") total = len(results) passed = sum(1 for r in results if r.passed) failed = total - passed print(f"\\nåèš:{total} æå:{passed} 倱æ:{failed}") def main(): import argparse parser = argparse.ArgumentParser(description="Markdown E2Eãã¹ãã©ã³ããŒ") parser.add_argument("scenarios", nargs="*", help="å®è¡ããã·ããªãªãã¡ã€ã«") parser.add_argument("--base-url", default=BASE_URL) parser.add_argument("--no-headless", action="store_true") parser.add_argument("--no-server", action="store_true") parser.add_argument("--no-seed", action="store_true") parser.add_argument("--timeout", type=int, default=30) args = parser.parse_args() global HEADLESS if args.no_headless: HEADLESS = False # ã·ããªãªãã¡ã€ã«ãååŸ if args.scenarios: files = [Path(s) for s in args.scenarios] else: files = sorted(SCENARIOS_DIR.glob("*.md")) if not files: print("ã·ããªãªãã¡ã€ã«ãèŠã€ãããŸãã") sys.exit(1) # ãã¹ãå®è¡ if not args.no_seed: ensure_seed_data() server_proc = None if not args.no_server: server_proc = start_django_server() try: results = run_scenarios(files, args.base_url, args.timeout * 1000) print_summary(results) sys.exit(0 if all(r.passed for r in results) else 1) finally: stop_django_server(server_proc) if __name__ == "__main__": main() åç»èšé²ã®ãã€ã³ã Playwrightã®åç»èšé²ã¯ ãã©ãŠã¶ã³ã³ããã¹ãåäœ ã§è¡ãããŸãã1ã€ã®ã·ããªãªãã¡ã€ã«å
ã®å
šã·ããªãªã1æ¬ã®åç»ã«ãŸãšãŸãããããã¹ã倱ææã®ãããã°ã容æã§ãã # ãã¡ã€ã«ããšã«ã³ã³ããã¹ããäœæ â 1ãã¡ã€ã« = 1åç» context = browser.new_context( record_video_dir=str(video_dir), record_video_size={"width": 1280, "height": 720}, ) Step 5: ã·ããªãªãã¡ã€ã«ãæžã ãããŸã§ã®ãã¬ãŒã ã¯ãŒã¯ã䜿ã£ãŠãå®éã®ã·ããªãªãæžããŠã¿ãŸãããã ãã¡ã€ã«åœåèŠå e2e/scenarios/ âââ 01_authentication.md # èªèšŒ âââ 02_agency_property.md # 代çåº ç©ä»¶ç®¡ç âââ 03_agency_request.md # 代çåº ç³è«ãã㌠âââ 04_admin_dashboard.md # 管çè
ããã·ã¥ããŒã âââ 05_admin_request.md # 管çè
ç³è«åŠç âââ 06_admin_agency.md # 管çè
代çåºç®¡ç çªå·ãã¬ãã£ãã¯ã¹ã§å®è¡é åºãå¶åŸ¡ããŸããèªèšŒãã¹ããæåã«å®è¡ããåæãšãªãæ©èœãå
ã«æ€èšŒããæ§æã§ãã ã·ããªãªã®æžãæ¹ã®ã³ã 1. 1ã·ããªãªã«è©°ã蟌ã¿ãããªã <!-- BAD: é·ãããã·ããªãª --> ## ã·ããªãª: ãã°ã€ã³ â ç©ä»¶ç»é² â ç³è« â æ¿èª â ãã°ã¢ãŠã 1. ... (50ã¹ããã) <!-- GOOD: è«ççãªãŸãšãŸãã§åå² --> ## ã·ããªãª: ç©ä»¶ç»é² 1. ... (15ã¹ããã) ## ã·ããªãª: éé»ç³è« 1. ... (12ã¹ããã) ãã ãã1ãã¡ã€ã«å
ã®ã·ããªãªã¯åãåç»ã«èšé²ãããããã é¢é£ããæäœãã㌠ã¯åããã¡ã€ã«ã«ãŸãšãããšè¯ãã§ãããã 2. ã»ã¬ã¯ã¿ã§ã¯ãªããŠãŒã¶ãŒãèŠããããã¹ãã䜿ã <!-- BAD: å®è£
äŸå --> 1. #login-btn ãã¯ãªãã¯ãã <!-- GOOD: ãŠãŒã¶ãŒèŠç¹ --> 1. ããã°ã€ã³ããã¿ã³ãã¯ãªãã¯ãã 3. ã¢ãµãŒã·ã§ã³ã¯å
·äœçã« <!-- BAD: ææ§ --> 1. ããŒãžã衚瀺ãããããšã確èªãã <!-- GOOD: å
·äœç --> 1. ãããã·ã¥ããŒãããšããããã¹ããç»é¢ã«è¡šç€ºãããŠããããšã確èªãã 2. URLã«ã/admin/dashboardããå«ãŸããããšã確èªãã 察å¿ããŠããã¹ããã衚çŸã®ãªãã¡ã¬ã³ã¹ ã«ããŽãª ã¹ãããäŸ ããã²ãŒã·ã§ã³ http://... ã«ã¢ã¯ã»ã¹ãã ã /path ã«ã¢ã¯ã»ã¹ãã ãªã³ã¯ã¯ãªã㯠ãã¡ãã¥ãŒåããªã³ã¯ãã¯ãªãã¯ãã ãã¿ã³ã¯ãªã㯠ãéä¿¡ããã¿ã³ãã¯ãªãã¯ãã ããã¹ãã¯ãªã㯠ãSKR-001ãããã¹ããã¯ãªãã¯ãã ã¿ãåæ¿ ãã¿ãåãã¿ããã¯ãªãã¯ãã ããã¹ãå
¥å é
ç®åã«ãå€ããå
¥åãã ã»ã¬ã¯ã ãé
ç®åãã§ãå€ããéžæãã æ¥ä»å
¥å ãé
ç®åãã« 2026-01-01 ãå
¥åãã ããã¹ãè¡šç€ºç¢ºèª ãããã¹ãããšããããã¹ãã衚瀺ãããŠããããšã確èªãã ããã¹ãéè¡šç€ºç¢ºèª ãããã¹ããã衚瀺ãããŠããªãããšã確èªãã URLç¢ºèª URLã«ã/pathããå«ãŸããããšã確èªãã ãã°ã¢ãŠã ãã°ã¢ãŠããã åŸ
æ© 3ç§åŸ
〠Step 6: pre-commitããã¯ã§éçºã«çµã¿èŸŒã 倿Žãããã¡ã€ã«ã«å¿ããŠé¢é£ããã·ããªãªã ããèªåå®è¡ããpre-commitããã¯ãèšå®ã§ããŸãã #!/bin/bash # e2e/hooks/pre-commit-e2e.sh CHANGED_FILES=$(git diff --cached --name-only) SCENARIOS_TO_RUN="" for file in $CHANGED_FILES; do case "$file" in *auth* | *login* | *middleware*) SCENARIOS_TO_RUN="$SCENARIOS_TO_RUN e2e/scenarios/01_authentication.md" ;; *property*) SCENARIOS_TO_RUN="$SCENARIOS_TO_RUN e2e/scenarios/02_agency_property.md" ;; *request*) SCENARIOS_TO_RUN="$SCENARIOS_TO_RUN e2e/scenarios/03_agency_request.md" SCENARIOS_TO_RUN="$SCENARIOS_TO_RUN e2e/scenarios/05_admin_request.md" ;; *agency*) SCENARIOS_TO_RUN="$SCENARIOS_TO_RUN e2e/scenarios/06_admin_agency.md" ;; # ã³ã¢ã¢ãã«å€æŽæã¯ãã«ãªã°ã¬ãã·ã§ã³ *models* | *enums* | *config/*) python e2e/runner.py exit $? ;; esac done if [ -n "$SCENARIOS_TO_RUN" ]; then # éè€é€å»ããŠå®è¡ UNIQUE=$(echo "$SCENARIOS_TO_RUN" | tr ' ' '\\n' | sort -u | tr '\\n' ' ') python e2e/runner.py $UNIQUE exit $? fi echo "E2Eãã¹ã察象ã®å€æŽãªããã¹ãããããŸã" exit 0 .git/hooks/pre-commit ã«ã·ã³ããªãã¯ãªã³ã¯ã貌ããã pre-commit ãã¬ãŒã ã¯ãŒã¯ã§ç®¡çããŸãã ãããžã§ã¯ãæ§æãŸãšã e2e/ âââ config.py # ç°å¢èšå®ïŒURLãã¿ã€ã ã¢ãŠãããã¹ãã¢ã«ãŠã³ãçïŒ âââ parser.py # MarkdownããŒãµãŒïŒ~90è¡ïŒ âââ step_mapper.py # ã¹ãããããããŒïŒ~280è¡ïŒ âââ runner.py # ãã¹ãã©ã³ããŒïŒ~280è¡ïŒ âââ requirements.txt # playwright>=1.40.0 âââ hooks/ â âââ pre-commit-e2e.sh âââ scenarios/ â âââ 01_authentication.md â âââ 02_agency_property.md â âââ ... âââ test-results/ # åç»ã»ã¹ã¯ãªãŒã³ã·ã§ããåºåå
å
šäœã§çŽ650è¡ã®Pythonã³ãŒãã§ãã å®è¡æ¹æ³ # Playwrightã®ã€ã³ã¹ããŒã« pip install playwright playwright install chromium # å
šã·ããªãªå®è¡ python e2e/runner.py # ç¹å®ã·ããªãªã®ã¿ python e2e/runner.py e2e/scenarios/01_authentication.md # ãã©ãŠã¶ã衚瀺ããŠå®è¡ïŒãããã°çšïŒ python e2e/runner.py --no-headless # ãµãŒããŒãæ¢ã«èµ·åããŠããå Žå python e2e/runner.py --no-server --no-seed å®è¡çµæïŒ ã·ãŒãããŒã¿ãæå
¥ããŸãã DjangoéçºãµãŒããŒãããŒã 8055 ã§èµ·åããŸãã ============================================================ å®è¡: èªèšŒæ©èœãã¹ã (01_authentication.md) ============================================================ ã·ããªãª: 管çè
ãã°ã€ã³æå â ãã°ã¢ãŠã â ãã¹ã¯ãŒãééã ã¹ããã 1: <http://localhost:8055/login/> ã«ã¢ã¯ã»ã¹ãã ... OK ã¹ããã 2: ã¡ãŒã«ã¢ãã¬ã¹ã«ãadmin@example.comããå
¥åãã ... OK ã¹ããã 3: ãã¹ã¯ãŒãã«ãadmin123ããå
¥åãã ... OK ã¹ããã 4: ããã°ã€ã³ããã¿ã³ãã¯ãªãã¯ãã ... OK ã¹ããã 5: ãããã·ã¥ããŒãããšããããã¹ãã衚瀺ãããŠãã ... OK ... ============================================================ ãã¹ãçµæãµããªãŒ ============================================================ [PASS] 01_authentication.md > 管çè
ãã°ã€ã³æå åç»: test-results/01_authentication/01_authentication.webm åèš: 1 æå: 1 倱æ: 0 åŸæ¥ã®E2Eãã¹ããšã®æ¯èŒ é
ç® Playwrightã³ãŒãçŽæžã Markdownã·ããªãªé§å å¯èªæ§ ãšã³ãžãã¢ã®ã¿ 誰ã§ãèªãã èšè¿°é å€ãïŒã»ã¬ã¯ã¿æå®çïŒ å°ãªãïŒèªç¶èšèªïŒ ã¡ã³ããã³ã¹ ãã¹ãããšã«ä¿®æ£ Step Mapperã®1ç®æãä¿®æ£ æè»æ§ ç¡å¶é ãã¿ãŒã³å®çŸ©å
ã«éå® ãããã° ã¹ãããåäœã§è¿œè·¡å¯èœ åå·Š + åç»èšé² åŠç¿ã³ã¹ã Playwright APIã®çè§£ãå¿
èŠ æ¥æ¬èªãã³ãã¬ã«æ²¿ãã ã CIçµ±å æšæºç åå·Š æ¡åŒµã®ã¢ã€ã㢠æ°ããã¹ããããã¿ãŒã³ã®è¿œå step_mapper.py ã«æ£èŠè¡šçŸãšå®è¡ããžãã¯ã远å ããã ãã§ãã # äŸ: ãã§ãã¯ããã¯ã¹ã®æäœ m = re.search(r"[ãã](.+?)[ãã]ãã§ãã¯ããã¯ã¹ããã§ãã¯ãã", step) if m: page.get_by_label(m.group(1)).check() return # äŸ: ãã¡ã€ã«ã¢ããããŒã m = re.search(r"[ãã](.+?)[ãã]ã«\\s*[ãã](.+?)[ãã]\\s*ãã¢ããããŒããã", step) if m: page.get_by_label(m.group(1)).set_input_files(m.group(2)) return å€èšèªå¯Ÿå¿ ãã¿ãŒã³å®çŸ©ãå€éšãã¡ã€ã«ïŒYAMLçïŒã«åãåºãã°ãè±èªçã容æã«äœããŸãã # patterns_en.yaml navigation: - pattern: 'navigate to "(.*)"' action: goto link_click: - pattern: 'click "(.*)" link' action: click_link ãã¹ãããŒã¿ã®ãã©ã¡ãŒã¿å åææ¡ä»¶ã»ã¯ã·ã§ã³ãããã¹ãããŒã¿ãåçã«çæããä»çµã¿ã远å ããããšãã§ããŸãã ãŸãšã Markdownã·ããªãªé§åã®E2Eãã¹ãã¯ã 仿§ãšãã¹ããäžäœå ãããã¢ãããŒãã§ãã Markdownã§æžããæäœæé ããã®ãŸãŸãã¹ãã«ãªã éãšã³ãžãã¢ã§ããã¹ãã±ãŒã¹ãã¬ãã¥ãŒã»è¿œå ã§ãã Step Mapperã®ä¿®æ£1ç®æã§å
šãã¹ãã®æåã倿Žã§ãã åç»èšé²ã«ãã倱ææã®ãããã°ãçŽæç å
šäœ650è¡çšåºŠã®ã³ãŒãã§å®çŸå¯èœ Playwrightã®æè»ãªãã±ãŒã¿ãŒæŠç¥ïŒ get_by_role , get_by_label , get_by_text ïŒãšæ£èŠè¡šçŸããŒã¹ã®ãã¿ãŒã³ãããã®çµã¿åããã«ãããå°ãªãã³ãŒãéã§å®çšçãªãã¬ãŒã ã¯ãŒã¯ãæ§ç¯ã§ããŸãã ãã¹ãã仿§æžãšä¹é¢ããåé¡ã«æ©ãã§ãããªãããã²è©ŠããŠã¿ãŠãã ããã ã芧ããã ãããããšãããããŸãïŒ ãã®æçš¿ã¯ã圹ã«ç«ã¡ãŸãããïŒ åœ¹ã«ç«ã£ã 圹ã«ç«ããªãã£ã 0人ããã®æçš¿ã¯åœ¹ã«ç«ã£ããšèšã£ãŠããŸãã The post Markdownã§æžãE2Eãã¹ãïŒèªç¶èšèªã·ããªãªãPlaywrightã§èªåå®è¡ããæ¹æ³ first appeared on SIOS Tech Lab .
åç»
該åœããã³ã³ãã³ããèŠã€ãããŸããã§ãã















