“성공 문구가 보인다”는 말이 어느 행동의 결과인지 따져 보기 위해 독립 비교 모듈을 만들었다. 실제 브라우저 없이 전후 합성 관찰과 원래 기대, 전달 단계를 넣어 결과를 볼 수 있다. 읽은 변화와 원격 저장의 확정을 같은 것으로 표시하지 않는다.
이 모듈은 결과를 읽는 규칙만 가진다
assess_transition은 before·after·condition·phase를 받는다. 관찰에는 tab과 text, 이미 나뉜 route, 선언된 boolean 상태 목록이 있다. 실제 계정과 페이지를 읽는 콜백도 없고 클릭·대기·재시도 함수도 없다. 확인 규칙을 다른 행동과 섞지 않고 시험하기 위한 순수 함수다.
phase는 prepared·dispatched·unknown 중 하나다. 이 값도 호출자의 선언이므로 실제 전달을 인증하지 않는다. tab이 같다는 문자열 비교도 진짜 소유권을 증명하지 않는다. 연결기는 신뢰할 관찰과 원장을 가져와야 한다. 이 모듈은 그 전제를 대신 만들어 주는 실행 호스트가 아니다.
원래부터 맞던 조건은 거절한다
텍스트 기대는 공백과 대소문자를 정리해 포함 여부를 본다. before에서 이미 맞으면 EXPECTATION_ALREADY_TRUE다. after에도 그 문구가 있다는 사실만으로 새 행동을 성공 처리하지 않는다. 빈 기대 문자열도 모든 텍스트를 통과시키므로 입력 단계에서 거절한다.
전달 단계가 prepared뿐인데 after에 조건이 나타나면 condition_present_without_dispatch로 돌려준다. 기대가 맞는 것과 우리 행동이 전달됐다는 판단을 분리한다. 이미 다른 사람이 만든 결과일 수도 있다. 이 예제는 멋진 성공 표시보다 판단이 부족한 상태를 정확히 표현하는 데 초점을 뒀다.
경로와 상태는 각자의 비교를 쓴다
route는 origin·path·query로 나눈 자료다. origin과 path는 정확히 비교하고 요구 쿼리 쌍은 개수를 센다. 같은 쌍이 두 번 필요하면 실제에도 두 번 있어야 하며 추가 실제 값은 허용한다. 원래 URL 파싱과 퍼센트 디코딩, fragment 비교는 이 예제에 넣지 않았다.
state는 같은 이름과 역할, field가 정확히 하나일 때 기대 참값과 비교한다. field는 checked·expanded·selected로 제한한다. 같은 이름의 두 상태 중 유리한 하나만 골라 성공시키지 않는다. 선언된 자료를 비교하는 계약이며 실제 DOM과 화면의 유효성 검사는 연결기의 책임이다.
코드와 구현 살펴보기
독립 합성 전체 구현
"""Compare supplied synthetic before/after observations. No browser or delivery."""
from collections import Counter
class OutcomeError(ValueError):pass
def _keys(value,fields):
if not isinstance(value,dict) or set(value)!=set(fields):raise OutcomeError('INVALID_FIELDS')
def _text(value,empty=False):
if not isinstance(value,str) or len(value)>20000 or (not empty and not value.strip()):raise OutcomeError('INVALID_TEXT')
def _norm(value):return ' '.join(value.strip().split()).lower()
def _pairs(value):
if not isinstance(value,list) or len(value)>1000:raise OutcomeError('INVALID_QUERY')
for pair in value:
if not isinstance(pair,list) or len(pair)!=2:raise OutcomeError('INVALID_QUERY')
_text(pair[0]);_text(pair[1],True)
return Counter(tuple(pair) for pair in value)
def _route(value):
_keys(value,['origin','path','query']);_text(value['origin']);_text(value['path']);_pairs(value['query'])
def _observation(value):
_keys(value,['tab','text','route','states']);_text(value['tab']);_text(value['text'],True);_route(value['route'])
if not isinstance(value['states'],list) or len(value['states'])>1000:raise OutcomeError('INVALID_STATES')
for state in value['states']:
_keys(state,['name','role','field','value'])
for key in ['name','role']:_text(state[key])
if state['field'] not in ['checked','expanded','selected'] or type(state['value']) is not bool:raise OutcomeError('INVALID_STATE')
def _condition(value):
if not isinstance(value,dict):raise OutcomeError('INVALID_CONDITION')
kind=value.get('kind')
if kind=='text':_keys(value,['kind','value']);_text(value['value'])
elif kind=='route':_keys(value,['kind','route']);_route(value['route'])
elif kind=='state':
_keys(value,['kind','name','role','field','value'])
for key in ['name','role']:_text(value[key])
if value['field'] not in ['checked','expanded','selected'] or type(value['value']) is not bool:raise OutcomeError('INVALID_STATE')
else:raise OutcomeError('INVALID_CONDITION')
def _matches(observation,condition):
kind=condition['kind']
if kind=='text':return _norm(condition['value']) in _norm(observation['text'])
if kind=='route':
actual,expected=observation['route'],condition['route']
return actual['origin']==expected['origin'] and actual['path']==expected['path'] and not (_pairs(expected['query'])-_pairs(actual['query']))
matches=[s for s in observation['states'] if _norm(s['name'])==_norm(condition['name']) and _norm(s['role'])==_norm(condition['role']) and s['field']==condition['field']]
return len(matches)==1 and matches[0]['value'] is condition['value']
def assess_transition(before,after,condition,phase):
"""Metadata and phase are caller assertions; this function authenticates none."""
_observation(before);_observation(after);_condition(condition)
if before['tab']!=after['tab']:raise OutcomeError('OBSERVATION_SCOPE_CHANGED')
if phase not in ['prepared','dispatched','unknown']:raise OutcomeError('INVALID_PHASE')
if _matches(before,condition):raise OutcomeError('EXPECTATION_ALREADY_TRUE')
matches=_matches(after,condition)
verified=matches and phase in ['dispatched','unknown']
state='observed_transition' if verified else 'condition_present_without_dispatch' if matches else 'not_confirmed'
return {'state':state,'condition_matches_after':matches,'verified_transition':verified,'phase':phase,'automatic_replay':False,'proves_causality':False,'starts_an_action':False}
늦게 본 변화도 재전송 없이 표현한다
dispatched나 unknown으로 선언한 뒤 원래 조건이 거짓에서 참이 되면 observed_transition이다. 반환값의 automatic_replay는 항상 거짓이고 proves_causality도 거짓이다. 늦게 본 결과를 읽었다는 표시가 그 사이의 모든 인과관계와 원격 저장을 증명하지는 않는다.
21개 시험은 텍스트·유일 상태·경로 일치와 이미 참인 조건, 아직 맞지 않는 결과, 준비 단계만의 결과를 다룬다. 닮은 origin과 경로, 반복 쿼리 개수와 순서, 중복 상태·범위 변경·잘못된 자료형·미지 키도 확인한다. 입력을 바꾸지 않고 실제 행동을 만들지 않는 결과까지 시험했다.
아래 선택기는 다섯 로컬 기록을 읽는다
새 결과와 이미 참인 조건, 확인되지 않은 결과, 준비만 한 상태, 늦게 읽은 변화를 합성 records로 만들었다. 실제 Python 실행으로 생성했고 선택기는 그 자료만 보여 준다. 방문자 계정과 페이지를 읽거나 원격 서버의 저장을 판정하는 화면이 아니다.
ZIP을 만든 뒤 항목별 해시를 맞추고 새 폴더에 추출해 같은 21개 시험을 실행한다. 두 언어의 실제 인라인 선택 코드는 DOM 대역에서 기록과 대조한다. 이 시험을 실브라우저 렌더링이나 현재 외부 서비스 발행의 검수로 확대하지 않는다. 코드 실행과 저장된 기록 표시가 각각 무엇을 확인했는지 남긴다.
자기 업무에서는 최종 결과를 더 연결한다
공개 ZIP에는 전체 구현·시험·합성 예제·기록 생성기와 README가 있고 표준 라이브러리만 쓴다. 수정·재사용할 수 있다. 원장 지속 저장과 시간 제한, 중지와 인증, 실제 주소 파싱은 구현하지 않았다. 이러한 기능은 업무에 맞는 연결 계층에서 마련해야 한다.
특히 송금과 승인, 외부 발행처럼 결과가 중요한 일은 관찰 변화 뒤에 서비스의 실제 기록을 더 확인해야 한다. 다른 작업이 같은 문구를 만들 수도 있고 관찰 자체가 부분 자료일 수도 있다. 나는 이 모듈을 만능 성공 판정기로 소개하지 않고 어떤 조건을 왜 성공이라고 보는지 질문할 수 있는 검토 부품으로 공유한다.