Move all files to 2017/

This commit is contained in:
Oliver Davies 2025-09-29 22:25:17 +01:00
parent ac7370f67f
commit 2875863330
15717 changed files with 0 additions and 0 deletions

View file

@ -0,0 +1,333 @@
!"#$%&'()*+,-./
0123456789
:;<=>?@
ABCDEFGHIJKLMNOPQRSTUVWXYZ
[\]^_`
abcdefghijklmnopqrstuvwxyz
{|}~€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ¡¢£¤¥¦§¨©
ª
«¬­®¯°±
²³
´
µ
¶·¸
¹º
»
¼½¾
¿
ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ
×
ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö
÷
øùúûüýþÿĀāĂ㥹ĆćĈĉĊċČčĎďĐđĒēĔĕĖėĘęĚěĜĝĞğĠġĢģĤĥĦħĨĩĪīĬĭĮįİıIJijĴĵĶķĸĹĺĻļĽľĿŀŁłŃńŅņŇňʼnŊŋŌōŎŏŐőŒœŔŕŖŗŘřŚśŜŝŞşŠšŢţŤťŦŧŨũŪūŬŭŮůŰűŲųŴŵŶŷŸŹźŻżŽžſƀƁƂƃƄƅƆƇƈƉƊƋƌƍƎƏƐƑƒƓƔƕƖƗƘƙƚƛƜƝƞƟƠơƢƣƤƥƦƧƨƩƪƫƬƭƮƯưƱƲƳƴƵƶƷƸƹƺƻƼƽƾƿǀǁǂǃDŽDždžLJLjljNJNjnjǍǎǏǐǑǒǓǔǕǖǗǘǙǚǛǜǝǞǟǠǡǢǣǤǥǦǧǨǩǪǫǬǭǮǯǰDZDzdzǴǵǶǷǸǹǺǻǼǽǾǿȀȁȂȃȄȅȆȇȈȉȊȋȌȍȎȏȐȑȒȓȔȕȖȗȘșȚțȜȝȞȟȠȡȢȣȤȥȦȧȨȩȪȫȬȭȮȯȰȱȲȳȴȵȶȷȸȹȺȻȼȽȾȿɀɁɂɃɄɅɆɇɈɉɊɋɌɍɎɏɐɑɒɓɔɕɖɗɘəɚɛɜɝɞɟɠɡɢɣɤɥɦɧɨɩɪɫɬɭɮɯɰɱɲɳɴɵɶɷɸɹɺɻɼɽɾɿʀʁʂʃʄʅʆʇʈʉʊʋʌʍʎʏʐʑʒʓʔʕʖʗʘʙʚʛʜʝʞʟʠʡʢʣʤʥʦʧʨʩʪʫʬʭʮʯʰʱʲʳʴʵʶʷʸʹʺʻʼʽʾʿˀˁ
˂˃˄˅
ˆˇˈˉˊˋˌˍˎˏːˑ
˒˓˔˕˖˗˘˙˚˛˜˝˞˟
ˠˡˢˣˤ
˥˦˧˨˩˪˫
ˬ
˭
ˮ
˯˰˱˲˳˴˵˶˷˸˹˺˻˼˽˾˿
̴̵̶̷̸̡̢̧̨̛̖̗̘̙̜̝̞̟̠̣̤̥̦̩̪̫̬̭̮̯̰̱̲̳̹̺̻̼͇͈͉͍͎̀́̂̃̄̅̆̇̈̉̊̋̌̍̎̏̐̑̒̓̔̽̾̿̀́͂̓̈́͆͊͋͌̕̚ͅ͏͓͔͕͖͙͚͐͑͒͗͛ͣͤͥͦͧͨͩͪͫͬͭͮͯ͘͜͟͢͝͞͠͡ͰͱͲͳʹ
͵
Ͷͷͺͻͼͽ
;΄΅
Ά
·
ΈΉΊΌΎΏΐΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩΪΫάέήίΰαβγδεζηθικλμνξοπρςστυφχψωϊϋόύώϏϐϑϒϓϔϕϖϗϘϙϚϛϜϝϞϟϠϡϢϣϤϥϦϧϨϩϪϫϬϭϮϯϰϱϲϳϴϵ
϶
ϷϸϹϺϻϼϽϾϿЀЁЂЃЄЅІЇЈЉЊЋЌЍЎЏАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяѐёђѓєѕіїјљњћќѝўџѠѡѢѣѤѥѦѧѨѩѪѫѬѭѮѯѰѱѲѳѴѵѶѷѸѹѺѻѼѽѾѿҀҁ
҂
҃҄҅҆҇҈҉ҊҋҌҍҎҏҐґҒғҔҕҖҗҘҙҚқҜҝҞҟҠҡҢңҤҥҦҧҨҩҪҫҬҭҮүҰұҲҳҴҵҶҷҸҹҺһҼҽҾҿӀӁӂӃӄӅӆӇӈӉӊӋӌӍӎӏӐӑӒӓӔӕӖӗӘәӚӛӜӝӞӟӠӡӢӣӤӥӦӧӨөӪӫӬӭӮӯӰӱӲӳӴӵӶӷӸӹӺӻӼӽӾӿԀԁԂԃԄԅԆԇԈԉԊԋԌԍԎԏԐԑԒԓԔԕԖԗԘԙԚԛԜԝԞԟԠԡԢԣԤԥԱԲԳԴԵԶԷԸԹԺԻԼԽԾԿՀՁՂՃՄՅՆՇՈՉՊՋՌՍՎՏՐՑՒՓՔՕՖՙ
՚՛՜՝՞՟
աբգդեզէըթժիլխծկհձղճմյնշոչպջռսվտրցւփքօֆև
։֊
ְֱֲֳִֵֶַָֹֺֻּֽ֑֖֛֢֣֤֥֦֧֪֚֭֮֒֓֔֕֗֘֙֜֝֞֟֠֡֨֩֫֬֯
־
ֿ
׀
ׁׂ
׃
ׅׄ
׆
ׇאבגדהוזחטיךכלםמןנסעףפץצקרשתװױײ
׳״؀؁؂؃؆؇؈؉؊؋،؍؎؏
ؘؙؚؐؑؒؓؔؕؖؗ
؛؞؟
ءآأؤإئابةتثجحخدذرزسشصضطظعغػؼؽؾؿـفقكلمنهوىيًٌٍَُِّْٕٖٜٓٔٗ٘ٙٚٛٝٞ٠١٢٣٤٥٦٧٨٩
٪٫٬٭
ٮٯٰٱٲٳٴٵٶٷٸٹٺٻټٽپٿڀځڂڃڄڅچڇڈډڊڋڌڍڎڏڐڑڒړڔڕږڗژڙښڛڜڝڞڟڠڡڢڣڤڥڦڧڨکڪګڬڭڮگڰڱڲڳڴڵڶڷڸڹںڻڼڽھڿۀہۂۃۄۅۆۇۈۉۊۋیۍێۏېۑےۓ
۔
ەۖۗۘۙۚۛۜ
۝
۞ۣ۟۠ۡۢۤۥۦۧۨ
۩
۪ۭ۫۬ۮۯ۰۱۲۳۴۵۶۷۸۹ۺۻۼ
۽۾
ۿ
܀܁܂܃܄܅܆܇܈܉܊܋܌܍܏
ܐܑܒܓܔܕܖܗܘܙܚܛܜܝܞܟܠܡܢܣܤܥܦܧܨܩܪܫܬܭܮܯܱܴܷܸܹܻܼܾ݂݄݆݈ܰܲܳܵܶܺܽܿ݀݁݃݅݇݉݊ݍݎݏݐݑݒݓݔݕݖݗݘݙݚݛݜݝݞݟݠݡݢݣݤݥݦݧݨݩݪݫݬݭݮݯݰݱݲݳݴݵݶݷݸݹݺݻݼݽݾݿހށނރބޅކއވމފދތލގޏސޑޒޓޔޕޖޗޘޙޚޛޜޝޞޟޠޡޢޣޤޥަާިީުޫެޭޮޯްޱ߀߁߂߃߄߅߆߇߈߉ߊߋߌߍߎߏߐߑߒߓߔߕߖߗߘߙߚߛߜߝߞߟߠߡߢߣߤߥߦߧߨߩߪ߲߫߬߭߮߯߰߱߳ߴߵ
߶߷߸߹
ߺࠀࠁࠂࠃࠄࠅࠆࠇࠈࠉࠊࠋࠌࠍࠎࠏࠐࠑࠒࠓࠔࠕࠖࠗ࠘࠙ࠚࠛࠜࠝࠞࠟࠠࠡࠢࠣࠤࠥࠦࠧࠨࠩࠪࠫࠬ࠭
࠰࠱࠲࠳࠴࠵࠶࠷࠸࠹࠺࠻࠼࠽࠾
ऀँंःऄअआइईउऊऋऌऍऎएऐऑऒओऔकखगघङचछजझञटठडढणतथदधनऩपफबभमयरऱलळऴवशषसह़ऽािीुूृॄॅॆेैॉॊोौ्ॎॐ॒॑॓॔ॕक़ख़ग़ज़ड़ढ़फ़य़ॠॡॢॣ
।॥
०१२३४५६७८९
ॱॲॹॺॻॼॽॾॿঁংঃঅআইঈউঊঋঌএঐওঔকখগঘঙচছজঝঞটঠডঢণতথদধনপফবভমযরলশষসহ়ঽািীুূৃৄেৈোৌ্ৎৗড়ঢ়য়ৠৡৢৣ০১২৩৪৫৬৭৮৯ৰৱ
৲৳
৴৵৶৷৸৹
৺৻
ਁਂਃਅਆਇਈਉਊਏਐਓਔਕਖਗਘਙਚਛਜਝਞਟਠਡਢਣਤਥਦਧਨਪਫਬਭਮਯਰਲਲ਼ਵਸ਼ਸਹ਼ਾਿੀੁੂੇੈੋੌ੍ੑਖ਼ਗ਼ਜ਼ੜਫ਼੦੧੨੩੪੫੬੭੮੯ੰੱੲੳੴੵઁંઃઅઆઇઈઉઊઋઌઍએઐઑઓઔકખગઘઙચછજઝઞટઠડઢણતથદધનપફબભમયરલળવશષસહ઼ઽાિીુૂૃૄૅેૈૉોૌ્ૐૠૡૢૣ૦૧૨૩૪૫૬૭૮૯
ଁଂଃଅଆଇଈଉଊଋଌଏଐଓଔକଖଗଘଙଚଛଜଝଞଟଠଡଢଣତଥଦଧନପଫବଭମଯରଲଳଵଶଷସହ଼ଽାିୀୁୂୃୄେୈୋୌ୍ୖୗଡ଼ଢ଼ୟୠୡୢୣ୦୧୨୩୪୫୬୭୮୯
ୱஂஃஅஆஇஈஉஊஎஏஐஒஓஔகஙசஜஞடணதநனபமயரறலளழவஶஷஸஹாிீுூெேைொோௌ்ௐௗ௦௧௨௩௪௫௬௭௮௯௰௱௲
௳௴௵௶௷௸௹௺
ఁంఃఅఆఇఈఉఊఋఌఎఏఐఒఓఔకఖగఘఙచఛజఝఞటఠడఢణతథదధనపఫబభమయరఱలళవశషసహఽాిీుూృౄెేైొోౌ్ౕౖౘౙౠౡౢౣ౦౧౨౩౪౫౬౭౮౯౸౹౺౻౼౽౾
౿
ಂಃಅಆಇಈಉಊಋಌಎಏಐಒಓಔಕಖಗಘಙಚಛಜಝಞಟಠಡಢಣತಥದಧನಪಫಬಭಮಯರಱಲಳವಶಷಸಹ಼ಽಾಿೀುೂೃೄೆೇೈೊೋೌ್ೕೖೞೠೡೢೣ೦೧೨೩೪೫೬೭೮೯
ೱೲ
ംഃഅആഇഈഉഊഋഌഎഏഐഒഓഔകഖഗഘങചഛജഝഞടഠഡഢണതഥദധനപഫബഭമയരറലളഴവശഷസഹഽാിീുൂൃൄെേൈൊോൌ്ൗൠൡൢൣ൦൧൨൩൪൫൬൭൮൯൰൱൲൳൴൵
ൺൻർൽൾൿංඃඅආඇඈඉඊඋඌඍඎඏඐඑඒඓඔඕඖකඛගඝඞඟචඡජඣඤඥඦටඨඩඪණඬතථදධනඳපඵබභමඹයරලවශෂසහළෆ්ාැෑිීුූෘෙේෛොෝෞෟෲෳ
กขฃคฅฆงจฉชซฌญฎฏฐฑฒณดตถทธนบปผฝพฟภมยรฤลฦวศษสหฬอฮฯะัาำิีึืฺุู
฿
เแโใไๅๆ็่้๊๋์ํ๎
๐๑๒๓๔๕๖๗๘๙
๚๛
ກຂຄງຈຊຍດຕຖທນບປຜຝພຟມຢຣລວສຫອຮຯະັາຳິີຶືຸູົຼຽເແໂໃໄໆ່້໊໋໌ໍ໐໑໒໓໔໕໖໗໘໙ໜໝༀ
༁༂༃༄༅༆༇༈༉༊་༌།༎༏༐༑༒༓༔༕༖༗
༘༙
༚༛༜༝༞༟
༠༡༢༣༤༥༦༧༨༩༪༫༬༭༮༯༰༱༲༳
༺༻༼༽
༾༿ཀཁགགྷངཅཆཇཉཊཋཌཌྷཎཏཐདདྷནཔཕབབྷམཙཚཛཛྷཝཞཟའཡརལཤཥསཧཨཀྵཪཫཬཱཱཱིིུུྲྀཷླྀཹེཻོཽཾཿ྄ཱྀྀྂྃ
྆྇ྈྉྊྋྐྑྒྒྷྔྕྖྗྙྚྛྜྜྷྞྟྠྡྡྷྣྤྥྦྦྷྨྩྪྫྫྷྭྮྯྰྱྲླྴྵྶྷྸྐྵྺྻྼ
྾྿࿀࿁࿂࿃࿄࿅
࿇࿈࿉࿊࿋࿌࿎࿏࿐࿑࿒࿓࿔࿕࿖࿗࿘
ကခဂဃငစဆဇဈဉညဋဌဍဎဏတထဒဓနပဖဗဘမယရလဝသဟဠအဢဣဤဥဦဧဨဩဪါာိီုူေဲဳဴဵံ့း္်ျြွှဿ၀၁၂၃၄၅၆၇၈၉
၊။၌၍၎၏
ၐၑၒၓၔၕၖၗၘၙၚၛၜၝၞၟၠၡၢၣၤၥၦၧၨၩၪၫၬၭၮၯၰၱၲၳၴၵၶၷၸၹၺၻၼၽၾၿႀႁႂႃႄႅႆႇႈႉႊႋႌႍႎႏ႐႑႒႓႔႕႖႗႘႙ႚႛႜႝ
႞႟
ႠႡႢႣႤႥႦႧႨႩႪႫႬႭႮႯႰႱႲႳႴႵႶႷႸႹႺႻႼႽႾႿჀჁჂჃჄჅაბგდევზთიკლმნოპჟრსტუფქღყშჩცძწჭხჯჰჱჲჳჴჵჶჷჸჹჺ
ჼᄀᄁᄂᄃᄄᄅᄆᄇᄈᄉᄊᄋᄌᄍᄎᄏᄐᄑᄒᄓᄔᄕᄖᄗᄘᄙᄚᄛᄜᄝᄞᄟᄠᄡᄢᄣᄤᄥᄦᄧᄨᄩᄪᄫᄬᄭᄮᄯᄰᄱᄲᄳᄴᄵᄶᄷᄸᄹᄺᄻᄼᄽᄾᄿᅀᅁᅂᅃᅄᅅᅆᅇᅈᅉᅊᅋᅌᅍᅎᅏᅐᅑᅒᅓᅔᅕᅖᅗᅘᅙᅚᅛᅜᅝᅞᅟᅠᅡᅢᅣᅤᅥᅦᅧᅨᅩᅪᅫᅬᅭᅮᅯᅰᅱᅲᅳᅴᅵᅶᅷᅸᅹᅺᅻᅼᅽᅾᅿᆀᆁᆂᆃᆄᆅᆆᆇᆈᆉᆊᆋᆌᆍᆎᆏᆐᆑᆒᆓᆔᆕᆖᆗᆘᆙᆚᆛᆜᆝᆞᆟᆠᆡᆢᆣᆤᆥᆦᆧᆨᆩᆪᆫᆬᆭᆮᆯᆰᆱᆲᆳᆴᆵᆶᆷᆸᆹᆺᆻᆼᆽᆾᆿᇀᇁᇂᇃᇄᇅᇆᇇᇈᇉᇊᇋᇌᇍᇎᇏᇐᇑᇒᇓᇔᇕᇖᇗᇘᇙᇚᇛᇜᇝᇞᇟᇠᇡᇢᇣᇤᇥᇦᇧᇨᇩᇪᇫᇬᇭᇮᇯᇰᇱᇲᇳᇴᇵᇶᇷᇸᇹᇺᇻᇼᇽᇾᇿሀሁሂሃሄህሆሇለሉሊላሌልሎሏሐሑሒሓሔሕሖሗመሙሚማሜምሞሟሠሡሢሣሤሥሦሧረሩሪራሬርሮሯሰሱሲሳሴስሶሷሸሹሺሻሼሽሾሿቀቁቂቃቄቅቆቇቈቊቋቌቍቐቑቒቓቔቕቖቘቚቛቜቝበቡቢባቤብቦቧቨቩቪቫቬቭቮቯተቱቲታቴትቶቷቸቹቺቻቼችቾቿኀኁኂኃኄኅኆኇኈኊኋኌኍነኑኒናኔንኖኗኘኙኚኛኜኝኞኟአኡኢኣኤእኦኧከኩኪካኬክኮኯኰኲኳኴኵኸኹኺኻኼኽኾዀዂዃዄዅወዉዊዋዌውዎዏዐዑዒዓዔዕዖዘዙዚዛዜዝዞዟዠዡዢዣዤዥዦዧየዩዪያዬይዮዯደዱዲዳዴድዶዷዸዹዺዻዼዽዾዿጀጁጂጃጄጅጆጇገጉጊጋጌግጎጏጐጒጓጔጕጘጙጚጛጜጝጞጟጠጡጢጣጤጥጦጧጨጩጪጫጬጭጮጯጰጱጲጳጴጵጶጷጸጹጺጻጼጽጾጿፀፁፂፃፄፅፆፇፈፉፊፋፌፍፎፏፐፑፒፓፔፕፖፗፘፙፚ፟
፠፡።፣፤፥፦፧፨
፩፪፫፬፭፮፯፰፱፲፳፴፵፶፷፸፹፺፻፼ᎀᎁᎂᎃᎄᎅᎆᎇᎈᎉᎊᎋᎌᎍᎎᎏ
᎐᎑᎒᎓᎔᎕᎖᎗᎘᎙
ᎠᎡᎢᎣᎤᎥᎦᎧᎨᎩᎪᎫᎬᎭᎮᎯᎰᎱᎲᎳᎴᎵᎶᎷᎸᎹᎺᎻᎼᎽᎾᎿᏀᏁᏂᏃᏄᏅᏆᏇᏈᏉᏊᏋᏌᏍᏎᏏᏐᏑᏒᏓᏔᏕᏖᏗᏘᏙᏚᏛᏜᏝᏞᏟᏠᏡᏢᏣᏤᏥᏦᏧᏨᏩᏪᏫᏬᏭᏮᏯᏰᏱᏲᏳᏴ
ᐁᐂᐃᐄᐅᐆᐇᐈᐉᐊᐋᐌᐍᐎᐏᐐᐑᐒᐓᐔᐕᐖᐗᐘᐙᐚᐛᐜᐝᐞᐟᐠᐡᐢᐣᐤᐥᐦᐧᐨᐩᐪᐫᐬᐭᐮᐯᐰᐱᐲᐳᐴᐵᐶᐷᐸᐹᐺᐻᐼᐽᐾᐿᑀᑁᑂᑃᑄᑅᑆᑇᑈᑉᑊᑋᑌᑍᑎᑏᑐᑑᑒᑓᑔᑕᑖᑗᑘᑙᑚᑛᑜᑝᑞᑟᑠᑡᑢᑣᑤᑥᑦᑧᑨᑩᑪᑫᑬᑭᑮᑯᑰᑱᑲᑳᑴᑵᑶᑷᑸᑹᑺᑻᑼᑽᑾᑿᒀᒁᒂᒃᒄᒅᒆᒇᒈᒉᒊᒋᒌᒍᒎᒏᒐᒑᒒᒓᒔᒕᒖᒗᒘᒙᒚᒛᒜᒝᒞᒟᒠᒡᒢᒣᒤᒥᒦᒧᒨᒩᒪᒫᒬᒭᒮᒯᒰᒱᒲᒳᒴᒵᒶᒷᒸᒹᒺᒻᒼᒽᒾᒿᓀᓁᓂᓃᓄᓅᓆᓇᓈᓉᓊᓋᓌᓍᓎᓏᓐᓑᓒᓓᓔᓕᓖᓗᓘᓙᓚᓛᓜᓝᓞᓟᓠᓡᓢᓣᓤᓥᓦᓧᓨᓩᓪᓫᓬᓭᓮᓯᓰᓱᓲᓳᓴᓵᓶᓷᓸᓹᓺᓻᓼᓽᓾᓿᔀᔁᔂᔃᔄᔅᔆᔇᔈᔉᔊᔋᔌᔍᔎᔏᔐᔑᔒᔓᔔᔕᔖᔗᔘᔙᔚᔛᔜᔝᔞᔟᔠᔡᔢᔣᔤᔥᔦᔧᔨᔩᔪᔫᔬᔭᔮᔯᔰᔱᔲᔳᔴᔵᔶᔷᔸᔹᔺᔻᔼᔽᔾᔿᕀᕁᕂᕃᕄᕅᕆᕇᕈᕉᕊᕋᕌᕍᕎᕏᕐᕑᕒᕓᕔᕕᕖᕗᕘᕙᕚᕛᕜᕝᕞᕟᕠᕡᕢᕣᕤᕥᕦᕧᕨᕩᕪᕫᕬᕭᕮᕯᕰᕱᕲᕳᕴᕵᕶᕷᕸᕹᕺᕻᕼᕽᕾᕿᖀᖁᖂᖃᖄᖅᖆᖇᖈᖉᖊᖋᖌᖍᖎᖏᖐᖑᖒᖓᖔᖕᖖᖗᖘᖙᖚᖛᖜᖝᖞᖟᖠᖡᖢᖣᖤᖥᖦᖧᖨᖩᖪᖫᖬᖭᖮᖯᖰᖱᖲᖳᖴᖵᖶᖷᖸᖹᖺᖻᖼᖽᖾᖿᗀᗁᗂᗃᗄᗅᗆᗇᗈᗉᗊᗋᗌᗍᗎᗏᗐᗑᗒᗓᗔᗕᗖᗗᗘᗙᗚᗛᗜᗝᗞᗟᗠᗡᗢᗣᗤᗥᗦᗧᗨᗩᗪᗫᗬᗭᗮᗯᗰᗱᗲᗳᗴᗵᗶᗷᗸᗹᗺᗻᗼᗽᗾᗿᘀᘁᘂᘃᘄᘅᘆᘇᘈᘉᘊᘋᘌᘍᘎᘏᘐᘑᘒᘓᘔᘕᘖᘗᘘᘙᘚᘛᘜᘝᘞᘟᘠᘡᘢᘣᘤᘥᘦᘧᘨᘩᘪᘫᘬᘭᘮᘯᘰᘱᘲᘳᘴᘵᘶᘷᘸᘹᘺᘻᘼᘽᘾᘿᙀᙁᙂᙃᙄᙅᙆᙇᙈᙉᙊᙋᙌᙍᙎᙏᙐᙑᙒᙓᙔᙕᙖᙗᙘᙙᙚᙛᙜᙝᙞᙟᙠᙡᙢᙣᙤᙥᙦᙧᙨᙩᙪᙫᙬ
ᙯᙰᙱᙲᙳᙴᙵᙶᙷᙸᙹᙺᙻᙼᙽᙾᙿ
ᚁᚂᚃᚄᚅᚆᚇᚈᚉᚊᚋᚌᚍᚎᚏᚐᚑᚒᚓᚔᚕᚖᚗᚘᚙᚚ
᚛᚜
ᚠᚡᚢᚣᚤᚥᚦᚧᚨᚩᚪᚫᚬᚭᚮᚯᚰᚱᚲᚳᚴᚵᚶᚷᚸᚹᚺᚻᚼᚽᚾᚿᛀᛁᛂᛃᛄᛅᛆᛇᛈᛉᛊᛋᛌᛍᛎᛏᛐᛑᛒᛓᛔᛕᛖᛗᛘᛙᛚᛛᛜᛝᛞᛟᛠᛡᛢᛣᛤᛥᛦᛧᛨᛩᛪ
᛫᛬᛭
ᛮᛯᛰᜀᜁᜂᜃᜄᜅᜆᜇᜈᜉᜊᜋᜌᜎᜏᜐᜑᜒᜓ᜔ᜠᜡᜢᜣᜤᜥᜦᜧᜨᜩᜪᜫᜬᜭᜮᜯᜰᜱᜲᜳ᜴
᜵᜶
ᝀᝁᝂᝃᝄᝅᝆᝇᝈᝉᝊᝋᝌᝍᝎᝏᝐᝑᝒᝓᝠᝡᝢᝣᝤᝥᝦᝧᝨᝩᝪᝫᝬᝮᝯᝰᝲᝳកខគឃងចឆជឈញដឋឌឍណតថទធនបផពភមយរលវឝឞសហឡអឣឤឥឦឧឨឩឪឫឬឭឮឯឰឱឲឳ
ាិីឹឺុូួើឿៀេែៃោៅំះៈ៉៊់៌៍៎៏័៑្៓
។៕៖
៘៙៚៛
ៜ៝០១២៣៤៥៦៧៨៩៰៱៲៳៴៵៶៷៸៹
᠀᠁᠂᠃᠄᠅᠆᠇᠈᠉᠊
᠐᠑᠒᠓᠔᠕᠖᠗᠘᠙ᠠᠡᠢᠣᠤᠥᠦᠧᠨᠩᠪᠫᠬᠭᠮᠯᠰᠱᠲᠳᠴᠵᠶᠷᠸᠹᠺᠻᠼᠽᠾᠿᡀᡁᡂᡃᡄᡅᡆᡇᡈᡉᡊᡋᡌᡍᡎᡏᡐᡑᡒᡓᡔᡕᡖᡗᡘᡙᡚᡛᡜᡝᡞᡟᡠᡡᡢᡣᡤᡥᡦᡧᡨᡩᡪᡫᡬᡭᡮᡯᡰᡱᡲᡳᡴᡵᡶᡷᢀᢁᢂᢃᢄᢅᢆᢇᢈᢉᢊᢋᢌᢍᢎᢏᢐᢑᢒᢓᢔᢕᢖᢗᢘᢙᢚᢛᢜᢝᢞᢟᢠᢡᢢᢣᢤᢥᢦᢧᢨᢩᢪᢰᢱᢲᢳᢴᢵᢶᢷᢸᢹᢺᢻᢼᢽᢾᢿᣀᣁᣂᣃᣄᣅᣆᣇᣈᣉᣊᣋᣌᣍᣎᣏᣐᣑᣒᣓᣔᣕᣖᣗᣘᣙᣚᣛᣜᣝᣞᣟᣠᣡᣢᣣᣤᣥᣦᣧᣨᣩᣪᣫᣬᣭᣮᣯᣰᣱᣲᣳᣴᣵᤀᤁᤂᤃᤄᤅᤆᤇᤈᤉᤊᤋᤌᤍᤎᤏᤐᤑᤒᤓᤔᤕᤖᤗᤘᤙᤚᤛᤜᤠᤡᤢᤣᤤᤥᤦᤧᤨᤩᤪᤫᤰᤱᤲᤳᤴᤵᤶᤷᤸ᤻᤹᤺
᥀᥄᥅
᥆᥇᥈᥉᥊᥋᥌᥍᥎᥏ᥐᥑᥒᥓᥔᥕᥖᥗᥘᥙᥚᥛᥜᥝᥞᥟᥠᥡᥢᥣᥤᥥᥦᥧᥨᥩᥪᥫᥬᥭᥰᥱᥲᥳᥴᦀᦁᦂᦃᦄᦅᦆᦇᦈᦉᦊᦋᦌᦍᦎᦏᦐᦑᦒᦓᦔᦕᦖᦗᦘᦙᦚᦛᦜᦝᦞᦟᦠᦡᦢᦣᦤᦥᦦᦧᦨᦩᦪᦫᦰᦱᦲᦳᦴᦵᦶᦷᦸᦹᦺᦻᦼᦽᦾᦿᧀᧁᧂᧃᧄᧅᧆᧇᧈᧉ᧐᧑᧒᧓᧔᧕᧖᧗᧘᧙᧚
᧞᧟᧠᧡᧢᧣᧤᧥᧦᧧᧨᧩᧪᧫᧬᧭᧮᧯᧰᧱᧲᧳᧴᧵᧶᧷᧸᧹᧺᧻᧼᧽᧾᧿
ᨀᨁᨂᨃᨄᨅᨆᨇᨈᨉᨊᨋᨌᨍᨎᨏᨐᨑᨒᨓᨔᨕᨖᨘᨗᨙᨚᨛ
᨞᨟
ᨠᨡᨢᨣᨤᨥᨦᨧᨨᨩᨪᨫᨬᨭᨮᨯᨰᨱᨲᨳᨴᨵᨶᨷᨸᨹᨺᨻᨼᨽᨾᨿᩀᩁᩂᩃᩄᩅᩆᩇᩈᩉᩊᩋᩌᩍᩎᩏᩐᩑᩒᩓᩔᩕᩖᩗᩘᩙᩚᩛᩜᩝᩞ᩠ᩡᩢᩣᩤᩥᩦᩧᩨᩩᩪᩫᩬᩭᩮᩯᩰᩱᩲᩳᩴ᩿᩵᩶᩷᩸᩹᩺᩻᩼᪀᪁᪂᪃᪄᪅᪆᪇᪈᪉᪐᪑᪒᪓᪔᪕᪖᪗᪘᪙
᪠᪡᪢᪣᪤᪥᪦
᪨᪩᪪᪫᪬᪭
ᬀᬁᬂᬃᬄᬅᬆᬇᬈᬉᬊᬋᬌᬍᬎᬏᬐᬑᬒᬓᬔᬕᬖᬗᬘᬙᬚᬛᬜᬝᬞᬟᬠᬡᬢᬣᬤᬥᬦᬧᬨᬩᬪᬫᬬᬭᬮᬯᬰᬱᬲᬳ᬴ᬵᬶᬷᬸᬹᬺᬻᬼᬽᬾᬿᭀᭁᭂᭃ᭄ᭅᭆᭇᭈᭉᭊᭋ᭐᭑᭒᭓᭔᭕᭖᭗᭘᭙
᭚᭛᭜᭝᭞᭟᭠᭡᭢᭣᭤᭥᭦᭧᭨᭩᭪
᭬᭫᭭᭮᭯᭰᭱᭲᭳
᭴᭵᭶᭷᭸᭹᭺᭻᭼
ᮀᮁᮂᮃᮄᮅᮆᮇᮈᮉᮊᮋᮌᮍᮎᮏᮐᮑᮒᮓᮔᮕᮖᮗᮘᮙᮚᮛᮜᮝᮞᮟᮠᮡᮢᮣᮤᮥᮦᮧᮨᮩ᮪ᮮᮯ᮰᮱᮲᮳᮴᮵᮶᮷᮸᮹ᰀᰁᰂᰃᰄᰅᰆᰇᰈᰉᰊᰋᰌᰍᰎᰏᰐᰑᰒᰓᰔᰕᰖᰗᰘᰙᰚᰛᰜᰝᰞᰟᰠᰡᰢᰣᰤᰥᰦᰧᰨᰩᰪᰫᰬᰭᰮᰯᰰᰱᰲᰳᰴᰵᰶ᰷
᰻᰼᰽᰾᰿
᱀᱁᱂᱃᱄᱅᱆᱇᱈᱉ᱍᱎᱏ᱐᱑᱒᱓᱔᱕᱖᱗᱘᱙ᱚᱛᱜᱝᱞᱟᱠᱡᱢᱣᱤᱥᱦᱧᱨᱩᱪᱫᱬᱭᱮᱯᱰᱱᱲᱳᱴᱵᱶᱷᱸᱹᱺᱻᱼᱽ
᱾᱿
᳐᳑᳒
᳔᳕᳖᳗᳘᳙᳜᳝᳞᳟᳚᳛᳠᳡᳢᳣᳤᳥᳦᳧᳨ᳩᳪᳫᳬ᳭ᳮᳯᳰᳱᳲᴀᴁᴂᴃᴄᴅᴆᴇᴈᴉᴊᴋᴌᴍᴎᴏᴐᴑᴒᴓᴔᴕᴖᴗᴘᴙᴚᴛᴜᴝᴞᴟᴠᴡᴢᴣᴤᴥᴦᴧᴨᴩᴪᴫᴬᴭᴮᴯᴰᴱᴲᴳᴴᴵᴶᴷᴸᴹᴺᴻᴼᴽᴾᴿᵀᵁᵂᵃᵄᵅᵆᵇᵈᵉᵊᵋᵌᵍᵎᵏᵐᵑᵒᵓᵔᵕᵖᵗᵘᵙᵚᵛᵜᵝᵞᵟᵠᵡᵢᵣᵤᵥᵦᵧᵨᵩᵪᵫᵬᵭᵮᵯᵰᵱᵲᵳᵴᵵᵶᵷᵸᵹᵺᵻᵼᵽᵾᵿᶀᶁᶂᶃᶄᶅᶆᶇᶈᶉᶊᶋᶌᶍᶎᶏᶐᶑᶒᶓᶔᶕᶖᶗᶘᶙᶚᶛᶜᶝᶞᶟᶠᶡᶢᶣᶤᶥᶦᶧᶨᶩᶪᶫᶬᶭᶮᶯᶰᶱᶲᶳᶴᶵᶶᶷᶸᶹᶺᶻᶼᶽᶾᶿ᷐᷎᷂᷊᷏᷽᷿᷀᷁᷃᷄᷅᷆᷇᷈᷉᷋᷌᷑᷒ᷓᷔᷕᷖᷗᷘᷙᷚᷛᷜᷝᷞᷟᷠᷡᷢᷣᷤᷥᷦ᷾᷍ḀḁḂḃḄḅḆḇḈḉḊḋḌḍḎḏḐḑḒḓḔḕḖḗḘḙḚḛḜḝḞḟḠḡḢḣḤḥḦḧḨḩḪḫḬḭḮḯḰḱḲḳḴḵḶḷḸḹḺḻḼḽḾḿṀṁṂṃṄṅṆṇṈṉṊṋṌṍṎṏṐṑṒṓṔṕṖṗṘṙṚṛṜṝṞṟṠṡṢṣṤṥṦṧṨṩṪṫṬṭṮṯṰṱṲṳṴṵṶṷṸṹṺṻṼṽṾṿẀẁẂẃẄẅẆẇẈẉẊẋẌẍẎẏẐẑẒẓẔẕẖẗẘẙẚẛẜẝẞẟẠạẢảẤấẦầẨẩẪẫẬậẮắẰằẲẳẴẵẶặẸẹẺẻẼẽẾếỀềỂểỄễỆệỈỉỊịỌọỎỏỐốỒồỔổỖỗỘộỚớỜờỞởỠỡỢợỤụỦủỨứỪừỬửỮữỰựỲỳỴỵỶỷỸỹỺỻỼỽỾỿἀἁἂἃἄἅἆἇἈἉἊἋἌἍἎἏἐἑἒἓἔἕἘἙἚἛἜἝἠἡἢἣἤἥἦἧἨἩἪἫἬἭἮἯἰἱἲἳἴἵἶἷἸἹἺἻἼἽἾἿὀὁὂὃὄὅὈὉὊὋὌὍὐὑὒὓὔὕὖὗὙὛὝὟὠὡὢὣὤὥὦὧὨὩὪὫὬὭὮὯὰάὲέὴήὶίὸόὺύὼώᾀᾁᾂᾃᾄᾅᾆᾇᾈᾉᾊᾋᾌᾍᾎᾏᾐᾑᾒᾓᾔᾕᾖᾗᾘᾙᾚᾛᾜᾝᾞᾟᾠᾡᾢᾣᾤᾥᾦᾧᾨᾩᾪᾫᾬᾭᾮᾯᾰᾱᾲᾳᾴᾶᾷᾸᾹᾺΆᾼ
᾿῀῁
ῂῃῄῆῇῈΈῊΉῌ
῍῎῏
ῐῑῒΐῖῗῘῙῚΊ
῝῞῟
ῠῡῢΰῤῥῦῧῨῩῪΎῬ
῭΅`
ῲῳῴῶῷῸΌῺΏῼ
´῾           ​‌‍‎‏‐‑‒–—―‖‗‘’‚‛“”„‟†‡•‣․‥…‧

‪‫‬‭‮ ‰‱′″‴‵‶‷‸‹›※‼‽‾‿⁀⁁⁂⁃⁄⁅⁆⁇⁈⁉⁊⁋⁌⁍⁎⁏⁐⁑⁒⁓⁔⁕⁖⁗⁘⁙⁚⁛⁜⁝⁞ ⁠⁡⁢⁣⁤
⁰ⁱ⁴⁵⁶⁷⁸⁹
⁺⁻⁼⁽⁾
ⁿ₀₁₂₃₄₅₆₇₈₉
₊₋₌₍₎
ₐₑₒₓₔ
₠₡₢₣₤₥₦₧₨₩₪₫€₭₮₯₰₱₲₳₴₵₶₷₸
⃒⃓⃘⃙⃚⃐⃑⃔⃕⃖⃗⃛⃜⃝⃞⃟⃠⃡⃢⃣⃤⃥⃦⃪⃫⃨⃬⃭⃮⃯⃧⃩⃰
℀℁
℃℄℅℆
℈℉
ℊℋℌℍℎℏℐℑℒℓ
№℗℘
℞℟℠℡™℣
KÅℬℭ
ℯℰℱℲℳℴℵℶℷℸℹ
℺℻
ℼℽℾℿ
⅀⅁⅂⅃⅄
⅊⅋⅌⅍
⅐⅑⅒⅓⅔⅕⅖⅗⅘⅙⅚⅛⅜⅝⅞⅟ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅪⅫⅬⅭⅮⅯⅰⅱⅲⅳⅴⅵⅶⅷⅸⅹⅺⅻⅼⅽⅾⅿↀↁↂↃↄↅↆↇↈ↉
←↑→↓↔↕↖↗↘↙↚↛↜↝↞↟↠↡↢↣↤↥↦↧↨↩↪↫↬↭↮↯↰↱↲↳↴↵↶↷↸↹↺↻↼↽↾↿⇀⇁⇂⇃⇄⇅⇆⇇⇈⇉⇊⇋⇌⇍⇎⇏⇐⇑⇒⇓⇔⇕⇖⇗⇘⇙⇚⇛⇜⇝⇞⇟⇠⇡⇢⇣⇤⇥⇦⇧⇨⇩⇪⇫⇬⇭⇮⇯⇰⇱⇲⇳⇴⇵⇶⇷⇸⇹⇺⇻⇼⇽⇾⇿∀∁∂∃∄∅∆∇∈∉∊∋∌∍∎∏∐∑−∓∔∕∖∗∘∙√∛∜∝∞∟∠∡∢∣∤∥∦∧∨∩∪∫∬∭∮∯∰∱∲∳∴∵∶∷∸∹∺∻∼∽∾∿≀≁≂≃≄≅≆≇≈≉≊≋≌≍≎≏≐≑≒≓≔≕≖≗≘≙≚≛≜≝≞≟≠≡≢≣≤≥≦≧≨≩≪≫≬≭≮≯≰≱≲≳≴≵≶≷≸≹≺≻≼≽≾≿⊀⊁⊂⊃⊄⊅⊆⊇⊈⊉⊊⊋⊌⊍⊎⊏⊐⊑⊒⊓⊔⊕⊖⊗⊘⊙⊚⊛⊜⊝⊞⊟⊠⊡⊢⊣⊤⊥⊦⊧⊨⊩⊪⊫⊬⊭⊮⊯⊰⊱⊲⊳⊴⊵⊶⊷⊸⊹⊺⊻⊼⊽⊾⊿⋀⋁⋂⋃⋄⋅⋆⋇⋈⋉⋊⋋⋌⋍⋎⋏⋐⋑⋒⋓⋔⋕⋖⋗⋘⋙⋚⋛⋜⋝⋞⋟⋠⋡⋢⋣⋤⋥⋦⋧⋨⋩⋪⋫⋬⋭⋮⋯⋰⋱⋲⋳⋴⋵⋶⋷⋸⋹⋺⋻⋼⋽⋾⋿⌀⌁⌂⌃⌄⌅⌆⌇⌈⌉⌊⌋⌌⌍⌎⌏⌐⌑⌒⌓⌔⌕⌖⌗⌘⌙⌚⌛⌜⌝⌞⌟⌠⌡⌢⌣⌤⌥⌦⌧⌨〈〉⌫⌬⌭⌮⌯⌰⌱⌲⌳⌴⌵⌶⌷⌸⌹⌺⌻⌼⌽⌾⌿⍀⍁⍂⍃⍄⍅⍆⍇⍈⍉⍊⍋⍌⍍⍎⍏⍐⍑⍒⍓⍔⍕⍖⍗⍘⍙⍚⍛⍜⍝⍞⍟⍠⍡⍢⍣⍤⍥⍦⍧⍨⍩⍪⍫⍬⍭⍮⍯⍰⍱⍲⍳⍴⍵⍶⍷⍸⍹⍺⍻⍼⍽⍾⍿⎀⎁⎂⎃⎄⎅⎆⎇⎈⎉⎊⎋⎌⎍⎎⎏⎐⎑⎒⎓⎔⎕⎖⎗⎘⎙⎚⎛⎜⎝⎞⎟⎠⎡⎢⎣⎤⎥⎦⎧⎨⎩⎪⎫⎬⎭⎮⎯⎰⎱⎲⎳⎴⎵⎶⎷⎸⎹⎺⎻⎼⎽⎾⎿⏀⏁⏂⏃⏄⏅⏆⏇⏈⏉⏊⏋⏌⏍⏎⏏⏐⏑⏒⏓⏔⏕⏖⏗⏘⏙⏚⏛⏜⏝⏞⏟⏠⏡⏢⏣⏤⏥⏦⏧⏨␀␁␂␃␄␅␆␇␈␉␊␋␌␍␎␏␐␑␒␓␔␕␖␗␘␙␚␛␜␝␞␟␠␡␢␣␤␥␦⑀⑁⑂⑃⑄⑅⑆⑇⑈⑉⑊
①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳⑴⑵⑶⑷⑸⑹⑺⑻⑼⑽⑾⑿⒀⒁⒂⒃⒄⒅⒆⒇⒈⒉⒊⒋⒌⒍⒎⒏⒐⒑⒒⒓⒔⒕⒖⒗⒘⒙⒚⒛
⒜⒝⒞⒟⒠⒡⒢⒣⒤⒥⒦⒧⒨⒩⒪⒫⒬⒭⒮⒯⒰⒱⒲⒳⒴⒵ⒶⒷⒸⒹⒺⒻⒼⒽⒾⒿⓀⓁⓂⓃⓄⓅⓆⓇⓈⓉⓊⓋⓌⓍⓎⓏⓐⓑⓒⓓⓔⓕⓖⓗⓘⓙⓚⓛⓜⓝⓞⓟⓠⓡⓢⓣⓤⓥⓦⓧⓨⓩ
⓪⓫⓬⓭⓮⓯⓰⓱⓲⓳⓴⓵⓶⓷⓸⓹⓺⓻⓼⓽⓾⓿
─━│┃┄┅┆┇┈┉┊┋┌┍┎┏┐┑┒┓└┕┖┗┘┙┚┛├┝┞┟┠┡┢┣┤┥┦┧┨┩┪┫┬┭┮┯┰┱┲┳┴┵┶┷┸┹┺┻┼┽┾┿╀╁╂╃╄╅╆╇╈╉╊╋╌╍╎╏═║╒╓╔╕╖╗╘╙╚╛╜╝╞╟╠╡╢╣╤╥╦╧╨╩╪╫╬╭╮╯╰╱╲╳╴╵╶╷╸╹╺╻╼╽╾╿▀▁▂▃▄▅▆▇█▉▊▋▌▍▎▏▐░▒▓▔▕▖▗▘▙▚▛▜▝▞▟■□▢▣▤▥▦▧▨▩▪▫▬▭▮▯▰▱▲△▴▵▶▷▸▹►▻▼▽▾▿◀◁◂◃◄◅◆◇◈◉◊○◌◍◎●◐◑◒◓◔◕◖◗◘◙◚◛◜◝◞◟◠◡◢◣◤◥◦◧◨◩◪◫◬◭◮◯◰◱◲◳◴◵◶◷◸◹◺◻◼◽◾◿☀☁☂☃☄★☆☇☈☉☊☋☌☍☎☏☐☑☒☓☔☕☖☗☘☙☚☛☜☝☞☟☠☡☢☣☤☥☦☧☨☩☪☫☬☭☮☯☰☱☲☳☴☵☶☷☸☹☺☻☼☽☾☿♀♁♂♃♄♅♆♇♈♉♊♋♌♍♎♏♐♑♒♓♔♕♖♗♘♙♚♛♜♝♞♟♠♡♢♣♤♥♦♧♨♩♪♫♬♭♮♯♰♱♲♳♴♵♶♷♸♹♺♻♼♽♾♿⚀⚁⚂⚃⚄⚅⚆⚇⚈⚉⚊⚋⚌⚍⚎⚏⚐⚑⚒⚓⚔⚕⚖⚗⚘⚙⚚⚛⚜⚝⚞⚟⚠⚡⚢⚣⚤⚥⚦⚧⚨⚩⚪⚫⚬⚭⚮⚯⚰⚱⚲⚳⚴⚵⚶⚷⚸⚹⚺⚻⚼⚽⚾⚿⛀⛁⛂⛃⛄⛅⛆⛇⛈⛉⛊⛋⛌⛍⛏⛐⛑⛒⛓⛔⛕⛖⛗⛘⛙⛚⛛⛜⛝⛞⛟⛠⛡⛣⛨⛩⛪⛫⛬⛭⛮⛯⛰⛱⛲⛳⛴⛵⛶⛷⛸⛹⛺⛻⛼⛽⛾⛿✁✂✃✄✆✇✈✉✌✍✎✏✐✑✒✓✔✕✖✗✘✙✚✛✜✝✞✟✠✡✢✣✤✥✦✧✩✪✫✬✭✮✯✰✱✲✳✴✵✶✷✸✹✺✻✼✽✾✿❀❁❂❃❄❅❆❇❈❉❊❋❍❏❐❑❒❖❗❘❙❚❛❜❝❞❡❢❣❤❥❦❧❨❩❪❫❬❭❮❯❰❱❲❳❴❵
❶❷❸❹❺❻❼❽❾❿➀➁➂➃➄➅➆➇➈➉➊➋➌➍➎➏➐➑➒➓
➔➘➙➚➛➜➝➞➟➠➡➢➣➤➥➦➧➨➩➪➫➬➭➮➯➱➲➳➴➵➶➷➸➹➺➻➼➽➾⟀⟁⟂⟃⟄⟅⟆⟇⟈⟉⟊⟌⟐⟑⟒⟓⟔⟕⟖⟗⟘⟙⟚⟛⟜⟝⟞⟟⟠⟡⟢⟣⟤⟥⟦⟧⟨⟩⟪⟫⟬⟭⟮⟯⟰⟱⟲⟳⟴⟵⟶⟷⟸⟹⟺⟻⟼⟽⟾⟿⠀⠁⠂⠃⠄⠅⠆⠇⠈⠉⠊⠋⠌⠍⠎⠏⠐⠑⠒⠓⠔⠕⠖⠗⠘⠙⠚⠛⠜⠝⠞⠟⠠⠡⠢⠣⠤⠥⠦⠧⠨⠩⠪⠫⠬⠭⠮⠯⠰⠱⠲⠳⠴⠵⠶⠷⠸⠹⠺⠻⠼⠽⠾⠿⡀⡁⡂⡃⡄⡅⡆⡇⡈⡉⡊⡋⡌⡍⡎⡏⡐⡑⡒⡓⡔⡕⡖⡗⡘⡙⡚⡛⡜⡝⡞⡟⡠⡡⡢⡣⡤⡥⡦⡧⡨⡩⡪⡫⡬⡭⡮⡯⡰⡱⡲⡳⡴⡵⡶⡷⡸⡹⡺⡻⡼⡽⡾⡿⢀⢁⢂⢃⢄⢅⢆⢇⢈⢉⢊⢋⢌⢍⢎⢏⢐⢑⢒⢓⢔⢕⢖⢗⢘⢙⢚⢛⢜⢝⢞⢟⢠⢡⢢⢣⢤⢥⢦⢧⢨⢩⢪⢫⢬⢭⢮⢯⢰⢱⢲⢳⢴⢵⢶⢷⢸⢹⢺⢻⢼⢽⢾⢿⣀⣁⣂⣃⣄⣅⣆⣇⣈⣉⣊⣋⣌⣍⣎⣏⣐⣑⣒⣓⣔⣕⣖⣗⣘⣙⣚⣛⣜⣝⣞⣟⣠⣡⣢⣣⣤⣥⣦⣧⣨⣩⣪⣫⣬⣭⣮⣯⣰⣱⣲⣳⣴⣵⣶⣷⣸⣹⣺⣻⣼⣽⣾⣿⤀⤁⤂⤃⤄⤅⤆⤇⤈⤉⤊⤋⤌⤍⤎⤏⤐⤑⤒⤓⤔⤕⤖⤗⤘⤙⤚⤛⤜⤝⤞⤟⤠⤡⤢⤣⤤⤥⤦⤧⤨⤩⤪⤫⤬⤭⤮⤯⤰⤱⤲⤳⤴⤵⤶⤷⤸⤹⤺⤻⤼⤽⤾⤿⥀⥁⥂⥃⥄⥅⥆⥇⥈⥉⥊⥋⥌⥍⥎⥏⥐⥑⥒⥓⥔⥕⥖⥗⥘⥙⥚⥛⥜⥝⥞⥟⥠⥡⥢⥣⥤⥥⥦⥧⥨⥩⥪⥫⥬⥭⥮⥯⥰⥱⥲⥳⥴⥵⥶⥷⥸⥹⥺⥻⥼⥽⥾⥿⦀⦁⦂⦃⦄⦅⦆⦇⦈⦉⦊⦋⦌⦍⦎⦏⦐⦑⦒⦓⦔⦕⦖⦗⦘⦙⦚⦛⦜⦝⦞⦟⦠⦡⦢⦣⦤⦥⦦⦧⦨⦩⦪⦫⦬⦭⦮⦯⦰⦱⦲⦳⦴⦵⦶⦷⦸⦹⦺⦻⦼⦽⦾⦿⧀⧁⧂⧃⧄⧅⧆⧇⧈⧉⧊⧋⧌⧍⧎⧏⧐⧑⧒⧓⧔⧕⧖⧗⧘⧙⧚⧛⧜⧝⧞⧟⧠⧡⧢⧣⧤⧥⧦⧧⧨⧩⧪⧫⧬⧭⧮⧯⧰⧱⧲⧳⧴⧵⧶⧷⧸⧹⧺⧻⧼⧽⧾⧿⨀⨁⨂⨃⨄⨅⨆⨇⨈⨉⨊⨋⨌⨍⨎⨏⨐⨑⨒⨓⨔⨕⨖⨗⨘⨙⨚⨛⨜⨝⨞⨟⨠⨡⨢⨣⨤⨥⨦⨧⨨⨩⨪⨫⨬⨭⨮⨯⨰⨱⨲⨳⨴⨵⨶⨷⨸⨹⨺⨻⨼⨽⨾⨿⩀⩁⩂⩃⩄⩅⩆⩇⩈⩉⩊⩋⩌⩍⩎⩏⩐⩑⩒⩓⩔⩕⩖⩗⩘⩙⩚⩛⩜⩝⩞⩟⩠⩡⩢⩣⩤⩥⩦⩧⩨⩩⩪⩫⩬⩭⩮⩯⩰⩱⩲⩳⩴⩵⩶⩷⩸⩹⩺⩻⩼⩽⩾⩿⪀⪁⪂⪃⪄⪅⪆⪇⪈⪉⪊⪋⪌⪍⪎⪏⪐⪑⪒⪓⪔⪕⪖⪗⪘⪙⪚⪛⪜⪝⪞⪟⪠⪡⪢⪣⪤⪥⪦⪧⪨⪩⪪⪫⪬⪭⪮⪯⪰⪱⪲⪳⪴⪵⪶⪷⪸⪹⪺⪻⪼⪽⪾⪿⫀⫁⫂⫃⫄⫅⫆⫇⫈⫉⫊⫋⫌⫍⫎⫏⫐⫑⫒⫓⫔⫕⫖⫗⫘⫙⫚⫛⫝̸⫝⫞⫟⫠⫡⫢⫣⫤⫥⫦⫧⫨⫩⫪⫫⫬⫭⫮⫯⫰⫱⫲⫳⫴⫵⫶⫷⫸⫹⫺⫻⫼⫽⫾⫿⬀⬁⬂⬃⬄⬅⬆⬇⬈⬉⬊⬋⬌⬍⬎⬏⬐⬑⬒⬓⬔⬕⬖⬗⬘⬙⬚⬛⬜⬝⬞⬟⬠⬡⬢⬣⬤⬥⬦⬧⬨⬩⬪⬫⬬⬭⬮⬯⬰⬱⬲⬳⬴⬵⬶⬷⬸⬹⬺⬻⬼⬽⬾⬿⭀⭁⭂⭃⭄⭅⭆⭇⭈⭉⭊⭋⭌⭐⭑⭒⭓⭔⭕⭖⭗⭘⭙
ⰀⰁⰂⰃⰄⰅⰆⰇⰈⰉⰊⰋⰌⰍⰎⰏⰐⰑⰒⰓⰔⰕⰖⰗⰘⰙⰚⰛⰜⰝⰞⰟⰠⰡⰢⰣⰤⰥⰦⰧⰨⰩⰪⰫⰬⰭⰮⰰⰱⰲⰳⰴⰵⰶⰷⰸⰹⰺⰻⰼⰽⰾⰿⱀⱁⱂⱃⱄⱅⱆⱇⱈⱉⱊⱋⱌⱍⱎⱏⱐⱑⱒⱓⱔⱕⱖⱗⱘⱙⱚⱛⱜⱝⱞⱠⱡⱢⱣⱤⱥⱦⱧⱨⱩⱪⱫⱬⱭⱮⱯⱰⱱⱲⱳⱴⱵⱶⱷⱸⱹⱺⱻⱼⱽⱾⱿⲀⲁⲂⲃⲄⲅⲆⲇⲈⲉⲊⲋⲌⲍⲎⲏⲐⲑⲒⲓⲔⲕⲖⲗⲘⲙⲚⲛⲜⲝⲞⲟⲠⲡⲢⲣⲤⲥⲦⲧⲨⲩⲪⲫⲬⲭⲮⲯⲰⲱⲲⲳⲴⲵⲶⲷⲸⲹⲺⲻⲼⲽⲾⲿⳀⳁⳂⳃⳄⳅⳆⳇⳈⳉⳊⳋⳌⳍⳎⳏⳐⳑⳒⳓⳔⳕⳖⳗⳘⳙⳚⳛⳜⳝⳞⳟⳠⳡⳢⳣⳤ
⳥⳦⳧⳨⳩⳪
ⳫⳬⳭⳮ⳯⳰⳱
⳹⳺⳻⳼
⳾⳿
ⴀⴁⴂⴃⴄⴅⴆⴇⴈⴉⴊⴋⴌⴍⴎⴏⴐⴑⴒⴓⴔⴕⴖⴗⴘⴙⴚⴛⴜⴝⴞⴟⴠⴡⴢⴣⴤⴥⴰⴱⴲⴳⴴⴵⴶⴷⴸⴹⴺⴻⴼⴽⴾⴿⵀⵁⵂⵃⵄⵅⵆⵇⵈⵉⵊⵋⵌⵍⵎⵏⵐⵑⵒⵓⵔⵕⵖⵗⵘⵙⵚⵛⵜⵝⵞⵟⵠⵡⵢⵣⵤⵥⵯⶀⶁⶂⶃⶄⶅⶆⶇⶈⶉⶊⶋⶌⶍⶎⶏⶐⶑⶒⶓⶔⶕⶖⶠⶡⶢⶣⶤⶥⶦⶨⶩⶪⶫⶬⶭⶮⶰⶱⶲⶳⶴⶵⶶⶸⶹⶺⶻⶼⶽⶾⷀⷁⷂⷃⷄⷅⷆⷈⷉⷊⷋⷌⷍⷎⷐⷑⷒⷓⷔⷕⷖⷘⷙⷚⷛⷜⷝⷞⷠⷡⷢⷣⷤⷥⷦⷧⷨⷩⷪⷫⷬⷭⷮⷯⷰⷱⷲⷳⷴⷵⷶⷷⷸⷹⷺⷻⷼⷽⷾⷿ
⸀⸁⸂⸃⸄⸅⸆⸇⸈⸉⸊⸋⸌⸍⸎⸏⸐⸑⸒⸓⸔⸕⸖⸗⸘⸙⸚⸛⸜⸝⸞⸟⸠⸡⸢⸣⸤⸥⸦⸧⸨⸩⸪⸫⸬⸭⸮
⸰⸱⺀⺁⺂⺃⺄⺅⺆⺇⺈⺉⺊⺋⺌⺍⺎⺏⺐⺑⺒⺓⺔⺕⺖⺗⺘⺙⺛⺜⺝⺞⺟⺠⺡⺢⺣⺤⺥⺦⺧⺨⺩⺪⺫⺬⺭⺮⺯⺰⺱⺲⺳⺴⺵⺶⺷⺸⺹⺺⺻⺼⺽⺾⺿⻀⻁⻂⻃⻄⻅⻆⻇⻈⻉⻊⻋⻌⻍⻎⻏⻐⻑⻒⻓⻔⻕⻖⻗⻘⻙⻚⻛⻜⻝⻞⻟⻠⻡⻢⻣⻤⻥⻦⻧⻨⻩⻪⻫⻬⻭⻮⻯⻰⻱⻲⻳⼀⼁⼂⼃⼄⼅⼆⼇⼈⼉⼊⼋⼌⼍⼎⼏⼐⼑⼒⼓⼔⼕⼖⼗⼘⼙⼚⼛⼜⼝⼞⼟⼠⼡⼢⼣⼤⼥⼦⼧⼨⼩⼪⼫⼬⼭⼮⼯⼰⼱⼲⼳⼴⼵⼶⼷⼸⼹⼺⼻⼼⼽⼾⼿⽀⽁⽂⽃⽄⽅⽆⽇⽈⽉⽊⽋⽌⽍⽎⽏⽐⽑⽒⽓⽔⽕⽖⽗⽘⽙⽚⽛⽜⽝⽞⽟⽠⽡⽢⽣⽤⽥⽦⽧⽨⽩⽪⽫⽬⽭⽮⽯⽰⽱⽲⽳⽴⽵⽶⽷⽸⽹⽺⽻⽼⽽⽾⽿⾀⾁⾂⾃⾄⾅⾆⾇⾈⾉⾊⾋⾌⾍⾎⾏⾐⾑⾒⾓⾔⾕⾖⾗⾘⾙⾚⾛⾜⾝⾞⾟⾠⾡⾢⾣⾤⾥⾦⾧⾨⾩⾪⾫⾬⾭⾮⾯⾰⾱⾲⾳⾴⾵⾶⾷⾸⾹⾺⾻⾼⾽⾾⾿⿀⿁⿂⿃⿄⿅⿆⿇⿈⿉⿊⿋⿌⿍⿎⿏⿐⿑⿒⿓⿔⿕⿰⿱⿲⿳⿴⿵⿶⿷⿸⿹⿺⿻ 、。〃〄
々〆〇
〈〉《》「」『』【】〒〓〔〕〖〗〘〙〚〛〜〝〞〟〠
〡〢〣〤〥〦〧〨〩〪〭〮〯〫〬
〱〲〳〴〵
〶〷
〸〹〺〻〼
〽〾〿
ぁあぃいぅうぇえぉおかがきぎくぐけげこごさざしじすずせぜそぞただちぢっつづてでとどなにぬねのはばぱひびぴふぶぷへべぺほぼぽまみむめもゃやゅゆょよらりるれろゎわゐゑをんゔゕゖ゙゚
゛゜
ゝゞゟ
ァアィイゥウェエォオカガキギクグケゲコゴサザシジスズセゼソゾタダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミムメモャヤュユョヨラリルレロヮワヰヱヲンヴヵヶヷヸヹヺ
ーヽヾヿㄅㄆㄇㄈㄉㄊㄋㄌㄍㄎㄏㄐㄑㄒㄓㄔㄕㄖㄗㄘㄙㄚㄛㄜㄝㄞㄟㄠㄡㄢㄣㄤㄥㄦㄧㄨㄩㄪㄫㄬㄭㄱㄲㄳㄴㄵㄶㄷㄸㄹㄺㄻㄼㄽㄾㄿㅀㅁㅂㅃㅄㅅㅆㅇㅈㅉㅊㅋㅌㅍㅎㅏㅐㅑㅒㅓㅔㅕㅖㅗㅘㅙㅚㅛㅜㅝㅞㅟㅠㅡㅢㅣㅤㅥㅦㅧㅨㅩㅪㅫㅬㅭㅮㅯㅰㅱㅲㅳㅴㅵㅶㅷㅸㅹㅺㅻㅼㅽㅾㅿㆀㆁㆂㆃㆄㆅㆆㆇㆈㆉㆊㆋㆌㆍㆎ
㆐㆑
㆒㆓㆔㆕
㆖㆗㆘㆙㆚㆛㆜㆝㆞㆟
ㆠㆡㆢㆣㆤㆥㆦㆧㆨㆩㆪㆫㆬㆭㆮㆯㆰㆱㆲㆳㆴㆵㆶㆷ
㇀㇁㇂㇃㇄㇅㇆㇇㇈㇉㇊㇋㇌㇍㇎㇏㇐㇑㇒㇓㇔㇕㇖㇗㇘㇙㇚㇛㇜㇝㇞㇟㇠㇡㇢㇣
ㇰㇱㇲㇳㇴㇵㇶㇷㇸㇹㇺㇻㇼㇽㇾㇿ
㈀㈁㈂㈃㈄㈅㈆㈇㈈㈉㈊㈋㈌㈍㈎㈏㈐㈑㈒㈓㈔㈕㈖㈗㈘㈙㈚㈛㈜㈝㈞
㈠㈡㈢㈣㈤㈥㈦㈧㈨㈩
㈪㈫㈬㈭㈮㈯㈰㈱㈲㈳㈴㈵㈶㈷㈸㈹㈺㈻㈼㈽㈾㈿㉀㉁㉂㉃㉄㉅㉆㉇㉈㉉㉊㉋㉌㉍㉎㉏㉐
㉑㉒㉓㉔㉕㉖㉗㉘㉙㉚㉛㉜㉝㉞㉟
㉠㉡㉢㉣㉤㉥㉦㉧㉨㉩㉪㉫㉬㉭㉮㉯㉰㉱㉲㉳㉴㉵㉶㉷㉸㉹㉺㉻㉼㉽㉾㉿
㊀㊁㊂㊃㊄㊅㊆㊇㊈㊉
㊊㊋㊌㊍㊎㊏㊐㊑㊒㊓㊔㊕㊖㊗㊘㊙㊚㊛㊜㊝㊞㊟㊠㊡㊢㊣㊤㊥㊦㊧㊨㊩㊪㊫㊬㊭㊮㊯㊰
㊱㊲㊳㊴㊵㊶㊷㊸㊹㊺㊻㊼㊽㊾㊿
㋀㋁㋂㋃㋄㋅㋆㋇㋈㋉㋊㋋㋌㋍㋎㋏㋐㋑㋒㋓㋔㋕㋖㋗㋘㋙㋚㋛㋜㋝㋞㋟㋠㋡㋢㋣㋤㋥㋦㋧㋨㋩㋪㋫㋬㋭㋮㋯㋰㋱㋲㋳㋴㋵㋶㋷㋸㋹㋺㋻㋼㋽㋾㌀㌁㌂㌃㌄㌅㌆㌇㌈㌉㌊㌋㌌㌍㌎㌏㌐㌑㌒㌓㌔㌕㌖㌗㌘㌙㌚㌛㌜㌝㌞㌟㌠㌡㌢㌣㌤㌥㌦㌧㌨㌩㌪㌫㌬㌭㌮㌯㌰㌱㌲㌳㌴㌵㌶㌷㌸㌹㌺㌻㌼㌽㌾㌿㍀㍁㍂㍃㍄㍅㍆㍇㍈㍉㍊㍋㍌㍍㍎㍏㍐㍑㍒㍓㍔㍕㍖㍗㍘㍙㍚㍛㍜㍝㍞㍟㍠㍡㍢㍣㍤㍥㍦㍧㍨㍩㍪㍫㍬㍭㍮㍯㍰㍱㍲㍳㍴㍵㍶㍷㍸㍹㍺㍻㍼㍽㍾㍿㎀㎁㎂㎃㎄㎅㎆㎇㎈㎉㎊㎋㎌㎍㎎㎏㎐㎑㎒㎓㎔㎕㎖㎗㎘㎙㎚㎛㎜㎝㎞㎟㎠㎡㎢㎣㎤㎥㎦㎧㎨㎩㎪㎫㎬㎭㎮㎯㎰㎱㎲㎳㎴㎵㎶㎷㎸㎹㎺㎻㎼㎽㎾㎿㏀㏁㏂㏃㏄㏅㏆㏇㏈㏉㏊㏋㏌㏍㏎㏏㏐㏑㏒㏓㏔㏕㏖㏗㏘㏙㏚㏛㏜㏝㏞㏟㏠㏡㏢㏣㏤㏥㏦㏧㏨㏩㏪㏫㏬㏭㏮㏯㏰㏱㏲㏳㏴㏵㏶㏷㏸㏹㏺㏻㏼㏽㏾㏿
㐀䶵
䷀䷁䷂䷃䷄䷅䷆䷇䷈䷉䷊䷋䷌䷍䷎䷏䷐䷑䷒䷓䷔䷕䷖䷗䷘䷙䷚䷛䷜䷝䷞䷟䷠䷡䷢䷣䷤䷥䷦䷧䷨䷩䷪䷫䷬䷭䷮䷯䷰䷱䷲䷳䷴䷵䷶䷷䷸䷹䷺䷻䷼䷽䷾䷿
一鿋ꀀꀁꀂꀃꀄꀅꀆꀇꀈꀉꀊꀋꀌꀍꀎꀏꀐꀑꀒꀓꀔꀕꀖꀗꀘꀙꀚꀛꀜꀝꀞꀟꀠꀡꀢꀣꀤꀥꀦꀧꀨꀩꀪꀫꀬꀭꀮꀯꀰꀱꀲꀳꀴꀵꀶꀷꀸꀹꀺꀻꀼꀽꀾꀿꁀꁁꁂꁃꁄꁅꁆꁇꁈꁉꁊꁋꁌꁍꁎꁏꁐꁑꁒꁓꁔꁕꁖꁗꁘꁙꁚꁛꁜꁝꁞꁟꁠꁡꁢꁣꁤꁥꁦꁧꁨꁩꁪꁫꁬꁭꁮꁯꁰꁱꁲꁳꁴꁵꁶꁷꁸꁹꁺꁻꁼꁽꁾꁿꂀꂁꂂꂃꂄꂅꂆꂇꂈꂉꂊꂋꂌꂍꂎꂏꂐꂑꂒꂓꂔꂕꂖꂗꂘꂙꂚꂛꂜꂝꂞꂟꂠꂡꂢꂣꂤꂥꂦꂧꂨꂩꂪꂫꂬꂭꂮꂯꂰꂱꂲꂳꂴꂵꂶꂷꂸꂹꂺꂻꂼꂽꂾꂿꃀꃁꃂꃃꃄꃅꃆꃇꃈꃉꃊꃋꃌꃍꃎꃏꃐꃑꃒꃓꃔꃕꃖꃗꃘꃙꃚꃛꃜꃝꃞꃟꃠꃡꃢꃣꃤꃥꃦꃧꃨꃩꃪꃫꃬꃭꃮꃯꃰꃱꃲꃳꃴꃵꃶꃷꃸꃹꃺꃻꃼꃽꃾꃿꄀꄁꄂꄃꄄꄅꄆꄇꄈꄉꄊꄋꄌꄍꄎꄏꄐꄑꄒꄓꄔꄕꄖꄗꄘꄙꄚꄛꄜꄝꄞꄟꄠꄡꄢꄣꄤꄥꄦꄧꄨꄩꄪꄫꄬꄭꄮꄯꄰꄱꄲꄳꄴꄵꄶꄷꄸꄹꄺꄻꄼꄽꄾꄿꅀꅁꅂꅃꅄꅅꅆꅇꅈꅉꅊꅋꅌꅍꅎꅏꅐꅑꅒꅓꅔꅕꅖꅗꅘꅙꅚꅛꅜꅝꅞꅟꅠꅡꅢꅣꅤꅥꅦꅧꅨꅩꅪꅫꅬꅭꅮꅯꅰꅱꅲꅳꅴꅵꅶꅷꅸꅹꅺꅻꅼꅽꅾꅿꆀꆁꆂꆃꆄꆅꆆꆇꆈꆉꆊꆋꆌꆍꆎꆏꆐꆑꆒꆓꆔꆕꆖꆗꆘꆙꆚꆛꆜꆝꆞꆟꆠꆡꆢꆣꆤꆥꆦꆧꆨꆩꆪꆫꆬꆭꆮꆯꆰꆱꆲꆳꆴꆵꆶꆷꆸꆹꆺꆻꆼꆽꆾꆿꇀꇁꇂꇃꇄꇅꇆꇇꇈꇉꇊꇋꇌꇍꇎꇏꇐꇑꇒꇓꇔꇕꇖꇗꇘꇙꇚꇛꇜꇝꇞꇟꇠꇡꇢꇣꇤꇥꇦꇧꇨꇩꇪꇫꇬꇭꇮꇯꇰꇱꇲꇳꇴꇵꇶꇷꇸꇹꇺꇻꇼꇽꇾꇿꈀꈁꈂꈃꈄꈅꈆꈇꈈꈉꈊꈋꈌꈍꈎꈏꈐꈑꈒꈓꈔꈕꈖꈗꈘꈙꈚꈛꈜꈝꈞꈟꈠꈡꈢꈣꈤꈥꈦꈧꈨꈩꈪꈫꈬꈭꈮꈯꈰꈱꈲꈳꈴꈵꈶꈷꈸꈹꈺꈻꈼꈽꈾꈿꉀꉁꉂꉃꉄꉅꉆꉇꉈꉉꉊꉋꉌꉍꉎꉏꉐꉑꉒꉓꉔꉕꉖꉗꉘꉙꉚꉛꉜꉝꉞꉟꉠꉡꉢꉣꉤꉥꉦꉧꉨꉩꉪꉫꉬꉭꉮꉯꉰꉱꉲꉳꉴꉵꉶꉷꉸꉹꉺꉻꉼꉽꉾꉿꊀꊁꊂꊃꊄꊅꊆꊇꊈꊉꊊꊋꊌꊍꊎꊏꊐꊑꊒꊓꊔꊕꊖꊗꊘꊙꊚꊛꊜꊝꊞꊟꊠꊡꊢꊣꊤꊥꊦꊧꊨꊩꊪꊫꊬꊭꊮꊯꊰꊱꊲꊳꊴꊵꊶꊷꊸꊹꊺꊻꊼꊽꊾꊿꋀꋁꋂꋃꋄꋅꋆꋇꋈꋉꋊꋋꋌꋍꋎꋏꋐꋑꋒꋓꋔꋕꋖꋗꋘꋙꋚꋛꋜꋝꋞꋟꋠꋡꋢꋣꋤꋥꋦꋧꋨꋩꋪꋫꋬꋭꋮꋯꋰꋱꋲꋳꋴꋵꋶꋷꋸꋹꋺꋻꋼꋽꋾꋿꌀꌁꌂꌃꌄꌅꌆꌇꌈꌉꌊꌋꌌꌍꌎꌏꌐꌑꌒꌓꌔꌕꌖꌗꌘꌙꌚꌛꌜꌝꌞꌟꌠꌡꌢꌣꌤꌥꌦꌧꌨꌩꌪꌫꌬꌭꌮꌯꌰꌱꌲꌳꌴꌵꌶꌷꌸꌹꌺꌻꌼꌽꌾꌿꍀꍁꍂꍃꍄꍅꍆꍇꍈꍉꍊꍋꍌꍍꍎꍏꍐꍑꍒꍓꍔꍕꍖꍗꍘꍙꍚꍛꍜꍝꍞꍟꍠꍡꍢꍣꍤꍥꍦꍧꍨꍩꍪꍫꍬꍭꍮꍯꍰꍱꍲꍳꍴꍵꍶꍷꍸꍹꍺꍻꍼꍽꍾꍿꎀꎁꎂꎃꎄꎅꎆꎇꎈꎉꎊꎋꎌꎍꎎꎏꎐꎑꎒꎓꎔꎕꎖꎗꎘꎙꎚꎛꎜꎝꎞꎟꎠꎡꎢꎣꎤꎥꎦꎧꎨꎩꎪꎫꎬꎭꎮꎯꎰꎱꎲꎳꎴꎵꎶꎷꎸꎹꎺꎻꎼꎽꎾꎿꏀꏁꏂꏃꏄꏅꏆꏇꏈꏉꏊꏋꏌꏍꏎꏏꏐꏑꏒꏓꏔꏕꏖꏗꏘꏙꏚꏛꏜꏝꏞꏟꏠꏡꏢꏣꏤꏥꏦꏧꏨꏩꏪꏫꏬꏭꏮꏯꏰꏱꏲꏳꏴꏵꏶꏷꏸꏹꏺꏻꏼꏽꏾꏿꐀꐁꐂꐃꐄꐅꐆꐇꐈꐉꐊꐋꐌꐍꐎꐏꐐꐑꐒꐓꐔꐕꐖꐗꐘꐙꐚꐛꐜꐝꐞꐟꐠꐡꐢꐣꐤꐥꐦꐧꐨꐩꐪꐫꐬꐭꐮꐯꐰꐱꐲꐳꐴꐵꐶꐷꐸꐹꐺꐻꐼꐽꐾꐿꑀꑁꑂꑃꑄꑅꑆꑇꑈꑉꑊꑋꑌꑍꑎꑏꑐꑑꑒꑓꑔꑕꑖꑗꑘꑙꑚꑛꑜꑝꑞꑟꑠꑡꑢꑣꑤꑥꑦꑧꑨꑩꑪꑫꑬꑭꑮꑯꑰꑱꑲꑳꑴꑵꑶꑷꑸꑹꑺꑻꑼꑽꑾꑿꒀꒁꒂꒃꒄꒅꒆꒇꒈꒉꒊꒋꒌ
꒐꒑꒒꒓꒔꒕꒖꒗꒘꒙꒚꒛꒜꒝꒞꒟꒠꒡꒢꒣꒤꒥꒦꒧꒨꒩꒪꒫꒬꒭꒮꒯꒰꒱꒲꒳꒴꒵꒶꒷꒸꒹꒺꒻꒼꒽꒾꒿꓀꓁꓂꓃꓄꓅꓆
ꓐꓑꓒꓓꓔꓕꓖꓗꓘꓙꓚꓛꓜꓝꓞꓟꓠꓡꓢꓣꓤꓥꓦꓧꓨꓩꓪꓫꓬꓭꓮꓯꓰꓱꓲꓳꓴꓵꓶꓷꓸꓹꓺꓻꓼꓽ
꓾꓿
ꔀꔁꔂꔃꔄꔅꔆꔇꔈꔉꔊꔋꔌꔍꔎꔏꔐꔑꔒꔓꔔꔕꔖꔗꔘꔙꔚꔛꔜꔝꔞꔟꔠꔡꔢꔣꔤꔥꔦꔧꔨꔩꔪꔫꔬꔭꔮꔯꔰꔱꔲꔳꔴꔵꔶꔷꔸꔹꔺꔻꔼꔽꔾꔿꕀꕁꕂꕃꕄꕅꕆꕇꕈꕉꕊꕋꕌꕍꕎꕏꕐꕑꕒꕓꕔꕕꕖꕗꕘꕙꕚꕛꕜꕝꕞꕟꕠꕡꕢꕣꕤꕥꕦꕧꕨꕩꕪꕫꕬꕭꕮꕯꕰꕱꕲꕳꕴꕵꕶꕷꕸꕹꕺꕻꕼꕽꕾꕿꖀꖁꖂꖃꖄꖅꖆꖇꖈꖉꖊꖋꖌꖍꖎꖏꖐꖑꖒꖓꖔꖕꖖꖗꖘꖙꖚꖛꖜꖝꖞꖟꖠꖡꖢꖣꖤꖥꖦꖧꖨꖩꖪꖫꖬꖭꖮꖯꖰꖱꖲꖳꖴꖵꖶꖷꖸꖹꖺꖻꖼꖽꖾꖿꗀꗁꗂꗃꗄꗅꗆꗇꗈꗉꗊꗋꗌꗍꗎꗏꗐꗑꗒꗓꗔꗕꗖꗗꗘꗙꗚꗛꗜꗝꗞꗟꗠꗡꗢꗣꗤꗥꗦꗧꗨꗩꗪꗫꗬꗭꗮꗯꗰꗱꗲꗳꗴꗵꗶꗷꗸꗹꗺꗻꗼꗽꗾꗿꘀꘁꘂꘃꘄꘅꘆꘇꘈꘉꘊꘋꘌ
꘍꘎꘏
ꘐꘑꘒꘓꘔꘕꘖꘗꘘꘙꘚꘛꘜꘝꘞꘟ꘠꘡꘢꘣꘤꘥꘦꘧꘨꘩ꘪꘫꙀꙁꙂꙃꙄꙅꙆꙇꙈꙉꙊꙋꙌꙍꙎꙏꙐꙑꙒꙓꙔꙕꙖꙗꙘꙙꙚꙛꙜꙝꙞꙟꙢꙣꙤꙥꙦꙧꙨꙩꙪꙫꙬꙭꙮ꙯꙰꙱꙲
꙼꙽
ꙿꚀꚁꚂꚃꚄꚅꚆꚇꚈꚉꚊꚋꚌꚍꚎꚏꚐꚑꚒꚓꚔꚕꚖꚗꚠꚡꚢꚣꚤꚥꚦꚧꚨꚩꚪꚫꚬꚭꚮꚯꚰꚱꚲꚳꚴꚵꚶꚷꚸꚹꚺꚻꚼꚽꚾꚿꛀꛁꛂꛃꛄꛅꛆꛇꛈꛉꛊꛋꛌꛍꛎꛏꛐꛑꛒꛓꛔꛕꛖꛗꛘꛙꛚꛛꛜꛝꛞꛟꛠꛡꛢꛣꛤꛥꛦꛧꛨꛩꛪꛫꛬꛭꛮꛯ꛰꛱
꛲꛳꛴꛵꛶꛷꜀꜁꜂꜃꜄꜅꜆꜇꜈꜉꜊꜋꜌꜍꜎꜏꜐꜑꜒꜓꜔꜕꜖
ꜗꜘꜙꜚꜛꜜꜝꜞꜟ
꜠꜡
ꜢꜣꜤꜥꜦꜧꜨꜩꜪꜫꜬꜭꜮꜯꜰꜱꜲꜳꜴꜵꜶꜷꜸꜹꜺꜻꜼꜽꜾꜿꝀꝁꝂꝃꝄꝅꝆꝇꝈꝉꝊꝋꝌꝍꝎꝏꝐꝑꝒꝓꝔꝕꝖꝗꝘꝙꝚꝛꝜꝝꝞꝟꝠꝡꝢꝣꝤꝥꝦꝧꝨꝩꝪꝫꝬꝭꝮꝯꝰꝱꝲꝳꝴꝵꝶꝷꝸꝹꝺꝻꝼꝽꝾꝿꞀꞁꞂꞃꞄꞅꞆꞇꞈ
꞉꞊
Ꞌꞌꟻꟼꟽꟾꟿꠀꠁꠂꠃꠄꠅ꠆ꠇꠈꠉꠊꠋꠌꠍꠎꠏꠐꠑꠒꠓꠔꠕꠖꠗꠘꠙꠚꠛꠜꠝꠞꠟꠠꠡꠢꠣꠤꠥꠦꠧ
꠨꠩꠪꠫
꠰꠱꠲꠳꠴꠵
꠶꠷꠸꠹
ꡀꡁꡂꡃꡄꡅꡆꡇꡈꡉꡊꡋꡌꡍꡎꡏꡐꡑꡒꡓꡔꡕꡖꡗꡘꡙꡚꡛꡜꡝꡞꡟꡠꡡꡢꡣꡤꡥꡦꡧꡨꡩꡪꡫꡬꡭꡮꡯꡰꡱꡲꡳ
꡴꡵꡶꡷
ꢀꢁꢂꢃꢄꢅꢆꢇꢈꢉꢊꢋꢌꢍꢎꢏꢐꢑꢒꢓꢔꢕꢖꢗꢘꢙꢚꢛꢜꢝꢞꢟꢠꢡꢢꢣꢤꢥꢦꢧꢨꢩꢪꢫꢬꢭꢮꢯꢰꢱꢲꢳꢴꢵꢶꢷꢸꢹꢺꢻꢼꢽꢾꢿꣀꣁꣂꣃ꣄
꣎꣏
꣐꣑꣒꣓꣔꣕꣖꣗꣘꣙꣠꣡꣢꣣꣤꣥꣦꣧꣨꣩꣪꣫꣬꣭꣮꣯꣰꣱ꣲꣳꣴꣵꣶꣷ
꣸꣹꣺
ꣻ꤀꤁꤂꤃꤄꤅꤆꤇꤈꤉ꤊꤋꤌꤍꤎꤏꤐꤑꤒꤓꤔꤕꤖꤗꤘꤙꤚꤛꤜꤝꤞꤟꤠꤡꤢꤣꤤꤥꤦꤧꤨꤩꤪ꤫꤬꤭
꤮꤯
ꤰꤱꤲꤳꤴꤵꤶꤷꤸꤹꤺꤻꤼꤽꤾꤿꥀꥁꥂꥃꥄꥅꥆꥇꥈꥉꥊꥋꥌꥍꥎꥏꥐꥑꥒ꥓
ꥠꥡꥢꥣꥤꥥꥦꥧꥨꥩꥪꥫꥬꥭꥮꥯꥰꥱꥲꥳꥴꥵꥶꥷꥸꥹꥺꥻꥼꦀꦁꦂꦃꦄꦅꦆꦇꦈꦉꦊꦋꦌꦍꦎꦏꦐꦑꦒꦓꦔꦕꦖꦗꦘꦙꦚꦛꦜꦝꦞꦟꦠꦡꦢꦣꦤꦥꦦꦧꦨꦩꦪꦫꦬꦭꦮꦯꦰꦱꦲ꦳ꦴꦵꦶꦷꦸꦹꦺꦻꦼꦽꦾꦿ꧀
꧁꧂꧃꧄꧅꧆꧇꧈꧉꧊꧋꧌꧍
ꧏ꧐꧑꧒꧓꧔꧕꧖꧗꧘꧙
꧞꧟
ꨀꨁꨂꨃꨄꨅꨆꨇꨈꨉꨊꨋꨌꨍꨎꨏꨐꨑꨒꨓꨔꨕꨖꨗꨘꨙꨚꨛꨜꨝꨞꨟꨠꨡꨢꨣꨤꨥꨦꨧꨨꨩꨪꨫꨬꨭꨮꨯꨰꨱꨲꨳꨴꨵꨶꩀꩁꩂꩃꩄꩅꩆꩇꩈꩉꩊꩋꩌꩍ꩐꩑꩒꩓꩔꩕꩖꩗꩘꩙
꩜꩝꩞꩟
ꩠꩡꩢꩣꩤꩥꩦꩧꩨꩩꩪꩫꩬꩭꩮꩯꩰꩱꩲꩳꩴꩵꩶ
꩷꩸꩹
ꩺꩻꪀꪁꪂꪃꪄꪅꪆꪇꪈꪉꪊꪋꪌꪍꪎꪏꪐꪑꪒꪓꪔꪕꪖꪗꪘꪙꪚꪛꪜꪝꪞꪟꪠꪡꪢꪣꪤꪥꪦꪧꪨꪩꪪꪫꪬꪭꪮꪯꪰꪱꪴꪲꪳꪵꪶꪷꪸꪹꪺꪻꪼꪽꪾ꪿ꫀ꫁ꫂꫛꫜꫝ
꫞꫟
ꯀꯁꯂꯃꯄꯅꯆꯇꯈꯉꯊꯋꯌꯍꯎꯏꯐꯑꯒꯓꯔꯕꯖꯗꯘꯙꯚꯛꯜꯝꯞꯟꯠꯡꯢꯣꯤꯥꯦꯧꯨꯩꯪ
꯬꯭꯰꯱꯲꯳꯴꯵꯶꯷꯸꯹가힣ힰힱힲힳힴힵힶힷힸힹힺힻힼힽힾힿퟀퟁퟂퟃퟄퟅퟆퟋퟌퟍퟎퟏퟐퟑퟒퟓퟔퟕퟖퟗퟘퟙퟚퟛퟜퟝퟞퟟퟠퟡퟢퟣퟤퟥퟦퟧퟨퟩퟪퟫퟬퟭퟮퟯퟰퟱퟲퟳퟴퟵퟶퟷퟸퟹퟺퟻ

豈更車賈滑串句龜龜契金喇奈懶癩羅蘿螺裸邏樂洛烙珞落酪駱亂卵欄爛蘭鸞嵐濫藍襤拉臘蠟廊朗浪狼郎來冷勞擄櫓爐盧老蘆虜路露魯鷺碌祿綠菉錄鹿論壟弄籠聾牢磊賂雷壘屢樓淚漏累縷陋勒肋凜凌稜綾菱陵讀拏樂諾丹寧怒率異北磻便復不泌數索參塞省葉說殺辰沈拾若掠略亮兩凉梁糧良諒量勵呂女廬旅濾礪閭驪麗黎力曆歷轢年憐戀撚漣煉璉秊練聯輦蓮連鍊列劣咽烈裂說廉念捻殮簾獵令囹寧嶺怜玲瑩羚聆鈴零靈領例禮醴隸惡了僚寮尿料樂燎療蓼遼龍暈阮劉杻柳流溜琉留硫紐類六戮陸倫崙淪輪律慄栗率隆利吏履易李梨泥理痢罹裏裡里離匿溺吝燐璘藺隣鱗麟林淋臨立笠粒狀炙識什茶刺切度拓糖宅洞暴輻行降見廓兀嗀﨎﨏塚﨑晴﨓﨔凞猪益礼神祥福靖精羽﨟蘒﨡諸﨣﨤逸都﨧﨨﨩飯飼館鶴侮僧免勉勤卑喝嘆器塀墨層屮悔慨憎懲敏既暑梅海渚漢煮爫琢碑社祉祈祐祖祝禍禎穀突節練縉繁署者臭艹艹著褐視謁謹賓贈辶逸難響頻恵𤋮舘並况全侀充冀勇勺喝啕喙嗢塚墳奄奔婢嬨廒廙彩徭惘慎愈憎慠懲戴揄搜摒敖晴朗望杖歹殺流滛滋漢瀞煮瞧爵犯猪瑱甆画瘝瘟益盛直睊着磌窱節类絛練缾者荒華蝹襁覆視調諸請謁諾諭謹變贈輸遲醙鉶陼難靖韛響頋頻鬒龜𢡊𢡄𣏕㮝䀘䀹𥉉𥳐𧻓齃龎fffiflffifflſtstﬓﬔﬕﬖﬗיִﬞײַﬠﬡﬢﬣﬤﬥﬦﬧﬨ
שׁשׂשּׁשּׂאַאָאּבּגּדּהּוּזּטּיּךּכּלּמּנּסּףּפּצּקּרּשּתּוֹבֿכֿפֿﭏﭐﭑﭒﭓﭔﭕﭖﭗﭘﭙﭚﭛﭜﭝﭞﭟﭠﭡﭢﭣﭤﭥﭦﭧﭨﭩﭪﭫﭬﭭﭮﭯﭰﭱﭲﭳﭴﭵﭶﭷﭸﭹﭺﭻﭼﭽﭾﭿﮀﮁﮂﮃﮄﮅﮆﮇﮈﮉﮊﮋﮌﮍﮎﮏﮐﮑﮒﮓﮔﮕﮖﮗﮘﮙﮚﮛﮜﮝﮞﮟﮠﮡﮢﮣﮤﮥﮦﮧﮨﮩﮪﮫﮬﮭﮮﮯﮰﮱﯓﯔﯕﯖﯗﯘﯙﯚﯛﯜﯝﯞﯟﯠﯡﯢﯣﯤﯥﯦﯧﯨﯩﯪﯫﯬﯭﯮﯯﯰﯱﯲﯳﯴﯵﯶﯷﯸﯹﯺﯻﯼﯽﯾﯿﰀﰁﰂﰃﰄﰅﰆﰇﰈﰉﰊﰋﰌﰍﰎﰏﰐﰑﰒﰓﰔﰕﰖﰗﰘﰙﰚﰛﰜﰝﰞﰟﰠﰡﰢﰣﰤﰥﰦﰧﰨﰩﰪﰫﰬﰭﰮﰯﰰﰱﰲﰳﰴﰵﰶﰷﰸﰹﰺﰻﰼﰽﰾﰿﱀﱁﱂﱃﱄﱅﱆﱇﱈﱉﱊﱋﱌﱍﱎﱏﱐﱑﱒﱓﱔﱕﱖﱗﱘﱙﱚﱛﱜﱝﱞﱟﱠﱡﱢﱣﱤﱥﱦﱧﱨﱩﱪﱫﱬﱭﱮﱯﱰﱱﱲﱳﱴﱵﱶﱷﱸﱹﱺﱻﱼﱽﱾﱿﲀﲁﲂﲃﲄﲅﲆﲇﲈﲉﲊﲋﲌﲍﲎﲏﲐﲑﲒﲓﲔﲕﲖﲗﲘﲙﲚﲛﲜﲝﲞﲟﲠﲡﲢﲣﲤﲥﲦﲧﲨﲩﲪﲫﲬﲭﲮﲯﲰﲱﲲﲳﲴﲵﲶﲷﲸﲹﲺﲻﲼﲽﲾﲿﳀﳁﳂﳃﳄﳅﳆﳇﳈﳉﳊﳋﳌﳍﳎﳏﳐﳑﳒﳓﳔﳕﳖﳗﳘﳙﳚﳛﳜﳝﳞﳟﳠﳡﳢﳣﳤﳥﳦﳧﳨﳩﳪﳫﳬﳭﳮﳯﳰﳱﳲﳳﳴﳵﳶﳷﳸﳹﳺﳻﳼﳽﳾﳿﴀﴁﴂﴃﴄﴅﴆﴇﴈﴉﴊﴋﴌﴍﴎﴏﴐﴑﴒﴓﴔﴕﴖﴗﴘﴙﴚﴛﴜﴝﴞﴟﴠﴡﴢﴣﴤﴥﴦﴧﴨﴩﴪﴫﴬﴭﴮﴯﴰﴱﴲﴳﴴﴵﴶﴷﴸﴹﴺﴻﴼﴽ
﴿
ﵐﵑﵒﵓﵔﵕﵖﵗﵘﵙﵚﵛﵜﵝﵞﵟﵠﵡﵢﵣﵤﵥﵦﵧﵨﵩﵪﵫﵬﵭﵮﵯﵰﵱﵲﵳﵴﵵﵶﵷﵸﵹﵺﵻﵼﵽﵾﵿﶀﶁﶂﶃﶄﶅﶆﶇﶈﶉﶊﶋﶌﶍﶎﶏﶒﶓﶔﶕﶖﶗﶘﶙﶚﶛﶜﶝﶞﶟﶠﶡﶢﶣﶤﶥﶦﶧﶨﶩﶪﶫﶬﶭﶮﶯﶰﶱﶲﶳﶴﶵﶶﶷﶸﶹﶺﶻﶼﶽﶾﶿﷀﷁﷂﷃﷄﷅﷆﷇﷰﷱﷲﷳﷴﷵﷶﷷﷸﷹﷺﷻ
﷼﷽
︐︑︒︓︔︕︖︗︘︙
︠︡︢︣︤︥︦
︰︱︲︳︴︵︶︷︸︹︺︻︼︽︾︿﹀﹁﹂﹃﹄﹅﹆﹇﹈﹉﹊﹋﹌﹍﹎﹏﹐﹑﹒﹔﹕﹖﹗﹘﹙﹚﹛﹜﹝﹞﹟﹠﹡﹢﹣﹤﹥﹦﹨﹩﹪﹫
ﹰﹱﹲﹳﹴﹶﹷﹸﹹﹺﹻﹼﹽﹾﹿﺀﺁﺂﺃﺄﺅﺆﺇﺈﺉﺊﺋﺌﺍﺎﺏﺐﺑﺒﺓﺔﺕﺖﺗﺘﺙﺚﺛﺜﺝﺞﺟﺠﺡﺢﺣﺤﺥﺦﺧﺨﺩﺪﺫﺬﺭﺮﺯﺰﺱﺲﺳﺴﺵﺶﺷﺸﺹﺺﺻﺼﺽﺾﺿﻀﻁﻂﻃﻄﻅﻆﻇﻈﻉﻊﻋﻌﻍﻎﻏﻐﻑﻒﻓﻔﻕﻖﻗﻘﻙﻚﻛﻜﻝﻞﻟﻠﻡﻢﻣﻤﻥﻦﻧﻨﻩﻪﻫﻬﻭﻮﻯﻰﻱﻲﻳﻴﻵﻶﻷﻸﻹﻺﻻﻼ

_
{|}~⦅⦆。「」、・
ヲァィゥェォャュョッーアイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン゙゚ᅠᄀᄁᆪᄂᆬᆭᄃᄄᄅᆰᆱᆲᆳᆴᆵᄚᄆᄇᄈᄡᄉᄊᄋᄌᄍᄎᄏᄐᄑ하ᅢᅣᅤᅥᅦᅧᅨᅩᅪᅫᅬᅭᅮᅯᅰᅱᅲᅳᅴᅵ
¢£¬ ̄¦¥₩←↑→↓■○<EFBFBD>
𐀀

View file

@ -0,0 +1,6 @@
name: 'Search Date Query Alter'
type: module
description: 'Test module that adds date conditions to node searches.'
package: Testing
version: VERSION
core: 8.x

View file

@ -0,0 +1,18 @@
<?php
/**
* @file
* Adds date conditions to node searches.
*/
use Drupal\Core\Database\Query\AlterableInterface;
/**
* Implements hook_query_TAG_alter(): tag search_$type with $type node_search.
*/
function search_date_query_alter_query_search_node_search_alter(AlterableInterface $query) {
// Start date Sat, 19 Mar 2016 00:00:00 GMT.
$query->condition('n.created', 1458345600, '>=');
// End date Sun, 20 Mar 2016 00:00:00 GMT.
$query->condition('n.created', 1458432000, '<');
}

View file

@ -0,0 +1,6 @@
name: 'Search Embedded Form'
type: module
description: 'Support module for Search module testing of embedded forms.'
package: Testing
version: VERSION
core: 8.x

View file

@ -0,0 +1,18 @@
<?php
/**
* @file
* Test module implementing a form that can be embedded in search results.
*
* A sample use of an embedded form is an e-commerce site where each search
* result may include an embedded form with buttons like "Add to cart" for each
* individual product (node) listed in the search results.
*/
/**
* Adds the test form to search results.
*/
function search_embedded_form_preprocess_search_result(&$variables) {
$form = \Drupal::formBuilder()->getForm('Drupal\search_embedded_form\Form\SearchEmbeddedForm');
$variables['snippet'] = array_merge($variables['snippet'], $form);
}

View file

@ -0,0 +1,7 @@
search_embedded_form.test_embedded_form:
path: '/search_embedded_form'
defaults:
_title: 'Search_Embed_Form'
_form: '\Drupal\search_embedded_form\Form\SearchEmbeddedForm'
requirements:
_permission: 'search content'

View file

@ -0,0 +1,56 @@
<?php
namespace Drupal\search_embedded_form\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Form controller for search_embedded_form form.
*
* @internal
*/
class SearchEmbeddedForm extends FormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'search_embedded_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$count = \Drupal::state()->get('search_embedded_form.submit_count');
$form['name'] = [
'#type' => 'textfield',
'#title' => $this->t('Your name'),
'#maxlength' => 255,
'#default_value' => '',
'#required' => TRUE,
'#description' => $this->t('Times form has been submitted: %count', ['%count' => $count]),
];
$form['actions'] = ['#type' => 'actions'];
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Send away'),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$state = \Drupal::state();
$submit_count = (int) $state->get('search_embedded_form.submit_count');
$state->set('search_embedded_form.submit_count', $submit_count + 1);
$this->messenger()->addStatus($this->t('Test form was submitted'));
}
}

View file

@ -0,0 +1,7 @@
id: dummy_search_type
label: 'Dummy search type'
status: true
langcode: en
path: dummy_path
plugin: search_extra_type_search
configuration: { }

View file

@ -0,0 +1,18 @@
# Schema for the configuration files of the Search Extra Type module.
search_extra_type.settings:
type: mapping
label: 'Test search type settings'
mapping:
boost:
type: string
label: 'String'
# Plugin \Drupal\search_extra_type\Plugin\Search\SearchExtraTypeSearch
search.plugin.search_extra_type_search:
type: mapping
label: 'Extra type settings'
mapping:
boost:
type: string
label: 'Boost method'

View file

@ -0,0 +1,8 @@
name: 'Test Search Type'
type: module
description: 'Support module for Search module testing.'
package: Testing
version: VERSION
core: 8.x
dependencies:
- drupal:test_page_test

View file

@ -0,0 +1,128 @@
<?php
namespace Drupal\search_extra_type\Plugin\Search;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\UrlGeneratorTrait;
use Drupal\Core\Url;
use Drupal\search\Plugin\ConfigurableSearchPluginBase;
/**
* Executes a dummy keyword search.
*
* @SearchPlugin(
* id = "search_extra_type_search",
* title = @Translation("Dummy search type")
* )
*/
class SearchExtraTypeSearch extends ConfigurableSearchPluginBase {
use UrlGeneratorTrait;
/**
* {@inheritdoc}
*/
public function setSearch($keywords, array $parameters, array $attributes) {
if (empty($parameters['search_conditions'])) {
$parameters['search_conditions'] = '';
}
parent::setSearch($keywords, $parameters, $attributes);
}
/**
* Verifies if the given parameters are valid enough to execute a search for.
*
* @return bool
* TRUE if there are keywords or search conditions in the query.
*/
public function isSearchExecutable() {
return (bool) ($this->keywords || !empty($this->searchParameters['search_conditions']));
}
/**
* Execute the search.
*
* This is a dummy search, so when search "executes", we just return a dummy
* result containing the keywords and a list of conditions.
*
* @return array
* A structured list of search results
*/
public function execute() {
$results = [];
if (!$this->isSearchExecutable()) {
return $results;
}
return [
[
'link' => Url::fromRoute('test_page_test.test_page')->toString(),
'type' => 'Dummy result type',
'title' => 'Dummy title',
'snippet' => new FormattableMarkup("Dummy search snippet to display. Keywords: @keywords\n\nConditions: @search_parameters", ['@keywords' => $this->keywords, '@search_parameters' => print_r($this->searchParameters, TRUE)]),
],
];
}
/**
* {@inheritdoc}
*/
public function buildResults() {
$results = $this->execute();
$output['prefix']['#markup'] = '<h2>Test page text is here</h2> <ol class="search-results">';
foreach ($results as $entry) {
$output[] = [
'#theme' => 'search_result',
'#result' => $entry,
'#plugin_id' => 'search_extra_type_search',
];
}
$pager = [
'#type' => 'pager',
];
$output['suffix']['#markup'] = '</ol>' . \Drupal::service('renderer')->render($pager);
return $output;
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
// Output form for defining rank factor weights.
$form['extra_type_settings'] = [
'#type' => 'fieldset',
'#title' => t('Extra type settings'),
'#tree' => TRUE,
];
$form['extra_type_settings']['boost'] = [
'#type' => 'select',
'#title' => t('Boost method'),
'#options' => [
'bi' => t('Bistromathic'),
'ii' => t('Infinite Improbability'),
],
'#default_value' => $this->configuration['boost'],
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
$this->configuration['boost'] = $form_state->getValue(['extra_type_settings', 'boost']);
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'boost' => 'bi',
];
}
}

View file

@ -0,0 +1,6 @@
name: 'Test search entity langcode'
type: module
description: 'Support module for search module testing.'
package: Testing
version: VERSION
core: 8.x

View file

@ -0,0 +1,40 @@
<?php
/**
* @file
* Test module setting up two tests, one for checking if the entity $langcode is
* being passed on and another one sets up the alternate verb forms for the
* stemming test.
*/
/**
* Implements hook_search_preprocess().
*/
function search_langcode_test_search_preprocess($text, $langcode = NULL) {
if (isset($langcode) && $langcode == 'en') {
// Add the alternate verb forms for the word "testing".
if ($text == 'we are testing') {
$text .= ' test tested';
}
// Prints the langcode for testPreprocessLangcode() and adds some
// extra text.
else {
\Drupal::messenger()->addStatus('Langcode Preprocess Test: ' . $langcode);
$text .= 'Additional text';
}
}
// Prints the langcode for testPreprocessLangcode().
elseif (isset($langcode)) {
\Drupal::messenger()->addStatus('Langcode Preprocess Test: ' . $langcode);
// Preprocessing for the excerpt test.
if ($langcode == 'ex') {
$text = str_replace('finding', 'find', $text);
$text = str_replace('finds', 'find', $text);
$text = str_replace('dic', ' dependency injection container', $text);
$text = str_replace('hypertext markup language', 'html', $text);
}
}
return $text;
}

View file

@ -0,0 +1,6 @@
name: 'Test Search Query Alter'
type: module
description: 'Support module for Search module testing.'
package: Testing
version: VERSION
core: 8.x

View file

@ -0,0 +1,16 @@
<?php
/**
* @file
* Test module that alters search queries.
*/
use Drupal\Core\Database\Query\AlterableInterface;
/**
* Implements hook_query_TAG_alter(): tag search_$type with $type node_search.
*/
function search_query_alter_query_search_node_search_alter(AlterableInterface $query) {
// For testing purposes, restrict the query to node type 'article' only.
$query->condition('n.type', 'article');
}

View file

@ -0,0 +1,30 @@
<?php
namespace Drupal\Tests\search\Functional\Hal;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
use Drupal\Tests\search\Functional\Rest\SearchPageResourceTestBase;
/**
* @group hal
*/
class SearchPageHalJsonAnonTest extends SearchPageResourceTestBase {
use AnonResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal'];
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
}

View file

@ -0,0 +1,35 @@
<?php
namespace Drupal\Tests\search\Functional\Hal;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
use Drupal\Tests\search\Functional\Rest\SearchPageResourceTestBase;
/**
* @group hal
*/
class SearchPageHalJsonBasicAuthTest extends SearchPageResourceTestBase {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal', 'basic_auth'];
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}

View file

@ -0,0 +1,35 @@
<?php
namespace Drupal\Tests\search\Functional\Hal;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
use Drupal\Tests\search\Functional\Rest\SearchPageResourceTestBase;
/**
* @group hal
*/
class SearchPageHalJsonCookieTest extends SearchPageResourceTestBase {
use CookieResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal'];
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
}

View file

@ -0,0 +1,24 @@
<?php
namespace Drupal\Tests\search\Functional\Rest;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
/**
* @group rest
*/
class SearchPageJsonAnonTest extends SearchPageResourceTestBase {
use AnonResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
}

View file

@ -0,0 +1,34 @@
<?php
namespace Drupal\Tests\search\Functional\Rest;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group rest
*/
class SearchPageJsonBasicAuthTest extends SearchPageResourceTestBase {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['basic_auth'];
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}

View file

@ -0,0 +1,29 @@
<?php
namespace Drupal\Tests\search\Functional\Rest;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
/**
* @group rest
*/
class SearchPageJsonCookieTest extends SearchPageResourceTestBase {
use CookieResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
}

View file

@ -0,0 +1,111 @@
<?php
namespace Drupal\Tests\search\Functional\Rest;
use Drupal\search\Entity\SearchPage;
use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
abstract class SearchPageResourceTestBase extends EntityResourceTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['node', 'search'];
/**
* {@inheritdoc}
*/
protected static $entityTypeId = 'search_page';
/**
* @var \Drupal\search\SearchPageInterface
*/
protected $entity;
/**
* {@inheritdoc}
*/
protected function setUpAuthorization($method) {
switch ($method) {
case 'GET':
$this->grantPermissionsToTestedRole(['access content']);
break;
case 'POST':
case 'PATCH':
case 'DELETE':
$this->grantPermissionsToTestedRole(['administer search']);
break;
}
}
/**
* {@inheritdoc}
*/
protected function createEntity() {
$search_page = SearchPage::create([
'id' => 'hinode_search',
'plugin' => 'node_search',
'label' => 'Search of magnetic activity of the Sun',
'path' => 'sun',
]);
$search_page->save();
return $search_page;
}
/**
* {@inheritdoc}
*/
protected function getExpectedNormalizedEntity() {
return [
'configuration' => [
'rankings' => [],
],
'dependencies' => [
'module' => ['node'],
],
'id' => 'hinode_search',
'label' => 'Search of magnetic activity of the Sun',
'langcode' => 'en',
'path' => 'sun',
'plugin' => 'node_search',
'status' => TRUE,
'uuid' => $this->entity->uuid(),
'weight' => 0,
];
}
/**
* {@inheritdoc}
*/
protected function getNormalizedPostEntity() {
// @todo Update in https://www.drupal.org/node/2300677.
}
/**
* {@inheritdoc}
*/
protected function getExpectedUnauthorizedAccessMessage($method) {
if ($this->config('rest.settings')->get('bc_entity_resource_permissions')) {
return parent::getExpectedUnauthorizedAccessMessage($method);
}
switch ($method) {
case 'GET':
return "The 'access content' permission is required.";
default:
return parent::getExpectedUnauthorizedAccessMessage($method);
}
}
/**
* {@inheritdoc}
*/
protected function getExpectedUnauthorizedAccessCacheability() {
// @see \Drupal\search\SearchPageAccessControlHandler::checkAccess()
return parent::getExpectedUnauthorizedAccessCacheability()
->addCacheTags(['config:search.page.hinode_search']);
}
}

View file

@ -0,0 +1,26 @@
<?php
namespace Drupal\Tests\search\Functional\Rest;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
* @group rest
*/
class SearchPageXmlAnonTest extends SearchPageResourceTestBase {
use AnonResourceTestTrait;
use XmlEntityNormalizationQuirksTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'xml';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'text/xml; charset=UTF-8';
}

View file

@ -0,0 +1,36 @@
<?php
namespace Drupal\Tests\search\Functional\Rest;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
* @group rest
*/
class SearchPageXmlBasicAuthTest extends SearchPageResourceTestBase {
use BasicAuthResourceTestTrait;
use XmlEntityNormalizationQuirksTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['basic_auth'];
/**
* {@inheritdoc}
*/
protected static $format = 'xml';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'text/xml; charset=UTF-8';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}

View file

@ -0,0 +1,31 @@
<?php
namespace Drupal\Tests\search\Functional\Rest;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
* @group rest
*/
class SearchPageXmlCookieTest extends SearchPageResourceTestBase {
use CookieResourceTestTrait;
use XmlEntityNormalizationQuirksTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'xml';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'text/xml; charset=UTF-8';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
}

View file

@ -0,0 +1,122 @@
<?php
namespace Drupal\Tests\search\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Indexes content and tests the advanced search form.
*
* @group search
*/
class SearchAdvancedSearchFormTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['node', 'search', 'dblog'];
/**
* A node to use for testing.
*
* @var \Drupal\node\NodeInterface
*/
protected $node;
protected function setUp() {
parent::setUp();
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
$this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']);
// Create and log in user.
$test_user = $this->drupalCreateUser(['access content', 'search content', 'use advanced search', 'administer nodes']);
$this->drupalLogin($test_user);
// Create initial node.
$this->node = $this->drupalCreateNode();
// First update the index. This does the initial processing.
$this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex();
// Then, run the shutdown function. Testing is a unique case where indexing
// and searching has to happen in the same request, so running the shutdown
// function manually is needed to finish the indexing process.
search_update_totals();
}
/**
* Tests advanced search by node type.
*/
public function testNodeType() {
// Verify some properties of the node that was created.
$this->assertTrue($this->node->getType() == 'page', 'Node type is Basic page.');
$dummy_title = 'Lorem ipsum';
$this->assertNotEqual($dummy_title, $this->node->label(), "Dummy title doesn't equal node title.");
// Search for the dummy title with a GET query.
$this->drupalGet('search/node', ['query' => ['keys' => $dummy_title]]);
$this->assertNoText($this->node->label(), 'Basic page node is not found with dummy title.');
// Search for the title of the node with a GET query.
$this->drupalGet('search/node', ['query' => ['keys' => $this->node->label()]]);
$this->assertText($this->node->label(), 'Basic page node is found with GET query.');
// Search for the title of the node with a POST query.
$edit = ['or' => $this->node->label()];
$this->drupalPostForm('search/node', $edit, 'edit-submit--2');
$this->assertText($this->node->label(), 'Basic page node is found with POST query.');
// Search by node type.
$this->drupalPostForm('search/node', array_merge($edit, ['type[page]' => 'page']), 'edit-submit--2');
$this->assertText($this->node->label(), 'Basic page node is found with POST query and type:page.');
$this->drupalPostForm('search/node', array_merge($edit, ['type[article]' => 'article']), 'edit-submit--2');
$this->assertText('search yielded no results', 'Article node is not found with POST query and type:article.');
}
/**
* Tests that after submitting the advanced search form, the form is refilled.
*/
public function testFormRefill() {
$edit = [
'keys' => 'cat',
'or' => 'dog gerbil',
'phrase' => 'pets are nice',
'negative' => 'fish snake',
'type[page]' => 'page',
];
$this->drupalPostForm('search/node', $edit, 'edit-submit--2');
// Test that the encoded query appears in the page title. Only test the
// part not including the quote, because assertText() cannot seem to find
// the quote marks successfully.
$this->assertText('Search for cat dog OR gerbil -fish -snake');
// Verify that all of the form fields are filled out.
foreach ($edit as $key => $value) {
if ($key != 'type[page]') {
$elements = $this->xpath('//input[@name=:name]', [':name' => $key]);
$this->assertTrue(isset($elements[0]) && $elements[0]->getValue() == $value, "Field $key is set to $value");
}
else {
$elements = $this->xpath('//input[@name=:name]', [':name' => $key]);
$this->assertTrue(isset($elements[0]) && !empty($elements[0]->getAttribute('checked')), "Field $key is checked");
}
}
// Now test by submitting the or/not part of the query in the main
// search box, and verify that the advanced form is not filled out.
// (It shouldn't be filled out unless you submit values in those fields.)
$edit2 = ['keys' => 'cat dog OR gerbil -fish -snake'];
$this->drupalPostForm('search/node', $edit2, 'edit-submit--2');
$this->assertText('Search for cat dog OR gerbil -fish -snake');
foreach ($edit as $key => $value) {
if ($key != 'type[page]') {
$elements = $this->xpath('//input[@name=:name]', [':name' => $key]);
$this->assertFalse(isset($elements[0]) && $elements[0]->getValue() == $value, "Field $key is not set to $value");
}
}
}
}

View file

@ -0,0 +1,109 @@
<?php
namespace Drupal\Tests\search\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests if the search form block is available.
*
* @group search
*/
class SearchBlockTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['block', 'node', 'search', 'dblog'];
protected function setUp() {
parent::setUp();
// Create and log in user.
$admin_user = $this->drupalCreateUser(['administer blocks', 'search content']);
$this->drupalLogin($admin_user);
}
/**
* Test that the search form block can be placed and works.
*/
public function testSearchFormBlock() {
// Test availability of the search block in the admin "Place blocks" list.
$this->drupalGet('admin/structure/block');
$this->getSession()->getPage()->findLink('Place block')->click();
$this->assertLinkByHref('/admin/structure/block/add/search_form_block/classy', 0,
'Did not find the search block in block candidate list.');
$block = $this->drupalPlaceBlock('search_form_block');
$this->drupalGet('');
$this->assertText($block->label(), 'Block title was found.');
// Check that name attribute is not empty.
$pattern = "//input[@type='submit' and @name='']";
$elements = $this->xpath($pattern);
$this->assertTrue(empty($elements), 'The search input field does not have empty name attribute.');
// Test a normal search via the block form, from the front page.
$terms = ['keys' => 'test'];
$this->drupalPostForm('', $terms, t('Search'));
$this->assertResponse(200);
$this->assertText('Your search yielded no results');
// Test a search from the block on a 404 page.
$this->drupalGet('foo');
$this->assertResponse(404);
$this->drupalPostForm(NULL, $terms, t('Search'));
$this->assertResponse(200);
$this->assertText('Your search yielded no results');
$visibility = $block->getVisibility();
$visibility['request_path']['pages'] = 'search';
$block->setVisibilityConfig('request_path', $visibility['request_path']);
$this->drupalPostForm('', $terms, t('Search'));
$this->assertResponse(200);
$this->assertText('Your search yielded no results');
// Confirm that the form submits to the default search page.
/** @var $search_page_repository \Drupal\search\SearchPageRepositoryInterface */
$search_page_repository = \Drupal::service('search.search_page_repository');
$entity_id = $search_page_repository->getDefaultSearchPage();
$this->assertEqual(
$this->getUrl(),
\Drupal::url('search.view_' . $entity_id, [], ['query' => ['keys' => $terms['keys']], 'absolute' => TRUE]),
'Submitted to correct URL.'
);
// Test an empty search via the block form, from the front page.
$terms = ['keys' => ''];
$this->drupalPostForm('', $terms, t('Search'));
$this->assertResponse(200);
$this->assertText('Please enter some keywords');
// Confirm that the user is redirected to the search page, when form is
// submitted empty.
$this->assertEqual(
$this->getUrl(),
\Drupal::url('search.view_' . $entity_id, [], ['query' => ['keys' => ''], 'absolute' => TRUE]),
'Redirected to correct URL.'
);
// Test that after entering a too-short keyword in the form, you can then
// search again with a longer keyword. First test using the block form.
$this->drupalPostForm('node', ['keys' => $this->randomMachineName(1)], t('Search'));
$this->assertText('You must include at least one keyword to match in the content', 'Keyword message is displayed when searching for short word');
$this->assertNoText(t('Please enter some keywords'), 'With short word entered, no keywords message is not displayed');
$this->drupalPostForm(NULL, ['keys' => $this->randomMachineName()], t('Search'), [], 'search-block-form');
$this->assertNoText('You must include at least one keyword to match in the content', 'Keyword message is not displayed when searching for long word after short word search');
// Same test again, using the search page form for the second search this
// time.
$this->drupalPostForm('node', ['keys' => $this->randomMachineName(1)], t('Search'));
$this->drupalPostForm(NULL, ['keys' => $this->randomMachineName()], t('Search'), [], 'search-form');
$this->assertNoText('You must include at least one keyword to match in the content', 'Keyword message is not displayed when searching for long word after short word search');
}
}

View file

@ -0,0 +1,116 @@
<?php
namespace Drupal\Tests\search\Functional;
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
use Drupal\comment\Tests\CommentTestTrait;
use Drupal\Tests\BrowserTestBase;
/**
* Tests that comment count display toggles properly on comment status of node.
*
* Issue 537278
*
* - Nodes with comment status set to Open should always how comment counts
* - Nodes with comment status set to Closed should show comment counts
* only when there are comments
* - Nodes with comment status set to Hidden should never show comment counts
*
* @group search
*/
class SearchCommentCountToggleTest extends BrowserTestBase {
use CommentTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['node', 'comment', 'search', 'dblog'];
/**
* A user with permission to search and post comments.
*
* @var \Drupal\user\UserInterface
*/
protected $searchingUser;
/**
* Array of nodes available to search.
*
* @var \Drupal\node\NodeInterface[]
*/
protected $searchableNodes;
protected function setUp() {
parent::setUp();
$this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']);
// Create searching user.
$this->searchingUser = $this->drupalCreateUser(['search content', 'access content', 'access comments', 'post comments', 'skip comment approval']);
// Log in with sufficient privileges.
$this->drupalLogin($this->searchingUser);
// Add a comment field.
$this->addDefaultCommentField('node', 'article');
// Create initial nodes.
$node_params = ['type' => 'article', 'body' => [['value' => 'SearchCommentToggleTestCase']]];
$this->searchableNodes['1 comment'] = $this->drupalCreateNode($node_params);
$this->searchableNodes['0 comments'] = $this->drupalCreateNode($node_params);
// Create a comment array
$edit_comment = [];
$edit_comment['subject[0][value]'] = $this->randomMachineName();
$edit_comment['comment_body[0][value]'] = $this->randomMachineName();
// Post comment to the test node with comment
$this->drupalPostForm('comment/reply/node/' . $this->searchableNodes['1 comment']->id() . '/comment', $edit_comment, t('Save'));
// First update the index. This does the initial processing.
$this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex();
// Then, run the shutdown function. Testing is a unique case where indexing
// and searching has to happen in the same request, so running the shutdown
// function manually is needed to finish the indexing process.
search_update_totals();
}
/**
* Verify that comment count display toggles properly on comment status of node
*/
public function testSearchCommentCountToggle() {
// Search for the nodes by string in the node body.
$edit = [
'keys' => "'SearchCommentToggleTestCase'",
];
$this->drupalGet('search/node');
// Test comment count display for nodes with comment status set to Open
$this->drupalPostForm(NULL, $edit, t('Search'));
$this->assertText(t('0 comments'), 'Empty comment count displays for nodes with comment status set to Open');
$this->assertText(t('1 comment'), 'Non-empty comment count displays for nodes with comment status set to Open');
// Test comment count display for nodes with comment status set to Closed
$this->searchableNodes['0 comments']->set('comment', CommentItemInterface::CLOSED);
$this->searchableNodes['0 comments']->save();
$this->searchableNodes['1 comment']->set('comment', CommentItemInterface::CLOSED);
$this->searchableNodes['1 comment']->save();
$this->drupalPostForm(NULL, $edit, t('Search'));
$this->assertNoText(t('0 comments'), 'Empty comment count does not display for nodes with comment status set to Closed');
$this->assertText(t('1 comment'), 'Non-empty comment count displays for nodes with comment status set to Closed');
// Test comment count display for nodes with comment status set to Hidden
$this->searchableNodes['0 comments']->set('comment', CommentItemInterface::HIDDEN);
$this->searchableNodes['0 comments']->save();
$this->searchableNodes['1 comment']->set('comment', CommentItemInterface::HIDDEN);
$this->searchableNodes['1 comment']->save();
$this->drupalPostForm(NULL, $edit, t('Search'));
$this->assertNoText(t('0 comments'), 'Empty comment count does not display for nodes with comment status set to Hidden');
$this->assertNoText(t('1 comment'), 'Non-empty comment count does not display for nodes with comment status set to Hidden');
}
}

View file

@ -0,0 +1,362 @@
<?php
namespace Drupal\Tests\search\Functional;
use Behat\Mink\Exception\ResponseTextException;
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
use Drupal\comment\Tests\CommentTestTrait;
use Drupal\field\Entity\FieldConfig;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\Traits\Core\CronRunTrait;
use Drupal\user\RoleInterface;
use Drupal\filter\Entity\FilterFormat;
/**
* Tests integration searching comments.
*
* @group search
*/
class SearchCommentTest extends BrowserTestBase {
use CommentTestTrait;
use CronRunTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['filter', 'node', 'comment', 'search'];
/**
* Test subject for comments.
*
* @var string
*/
protected $commentSubject;
/**
* ID for the administrator role.
*
* @var string
*/
protected $adminRole;
/**
* A user with various administrative permissions.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* Test node for searching.
*
* @var \Drupal\node\NodeInterface
*/
protected $node;
protected function setUp() {
parent::setUp();
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
$this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']);
$full_html_format = FilterFormat::create([
'format' => 'full_html',
'name' => 'Full HTML',
'weight' => 1,
'filters' => [],
]);
$full_html_format->save();
// Create and log in an administrative user having access to the Full HTML
// text format.
$permissions = [
'administer filters',
$full_html_format->getPermissionName(),
'administer permissions',
'create page content',
'post comments',
'skip comment approval',
'access comments',
];
$this->adminUser = $this->drupalCreateUser($permissions);
$this->drupalLogin($this->adminUser);
// Add a comment field.
$this->addDefaultCommentField('node', 'article');
}
/**
* Verify that comments are rendered using proper format in search results.
*/
public function testSearchResultsComment() {
$node_storage = $this->container->get('entity.manager')->getStorage('node');
// Create basic_html format that escapes all HTML.
$basic_html_format = FilterFormat::create([
'format' => 'basic_html',
'name' => 'Basic HTML',
'weight' => 1,
'filters' => [
'filter_html_escape' => ['status' => 1],
],
'roles' => [RoleInterface::AUTHENTICATED_ID],
]);
$basic_html_format->save();
$comment_body = 'Test comment body';
// Make preview optional.
$field = FieldConfig::loadByName('node', 'article', 'comment');
$field->setSetting('preview', DRUPAL_OPTIONAL);
$field->save();
// Allow anonymous users to search content.
$edit = [
RoleInterface::ANONYMOUS_ID . '[search content]' => 1,
RoleInterface::ANONYMOUS_ID . '[access comments]' => 1,
RoleInterface::ANONYMOUS_ID . '[post comments]' => 1,
];
$this->drupalPostForm('admin/people/permissions', $edit, t('Save permissions'));
// Create a node.
$node = $this->drupalCreateNode(['type' => 'article']);
// Post a comment using 'Full HTML' text format.
$edit_comment = [];
$edit_comment['subject[0][value]'] = 'Test comment subject';
$edit_comment['comment_body[0][value]'] = '<h1>' . $comment_body . '</h1>';
$full_html_format_id = 'full_html';
$edit_comment['comment_body[0][format]'] = $full_html_format_id;
$this->drupalPostForm('comment/reply/node/' . $node->id() . '/comment', $edit_comment, t('Save'));
// Post a comment with an evil script tag in the comment subject and a
// script tag nearby a keyword in the comment body. Use the 'FULL HTML' text
// format so the script tag stored.
$edit_comment2 = [];
$edit_comment2['subject[0][value]'] = "<script>alert('subjectkeyword');</script>";
$edit_comment2['comment_body[0][value]'] = "nearbykeyword<script>alert('somethinggeneric');</script>";
$edit_comment2['comment_body[0][format]'] = $full_html_format_id;
$this->drupalPostForm('comment/reply/node/' . $node->id() . '/comment', $edit_comment2, t('Save'));
// Post a comment with a keyword inside an evil script tag in the comment
// body. Use the 'FULL HTML' text format so the script tag is stored.
$edit_comment3 = [];
$edit_comment3['subject[0][value]'] = 'asubject';
$edit_comment3['comment_body[0][value]'] = "<script>alert('insidekeyword');</script>";
$edit_comment3['comment_body[0][format]'] = $full_html_format_id;
$this->drupalPostForm('comment/reply/node/' . $node->id() . '/comment', $edit_comment3, t('Save'));
// Invoke search index update.
$this->drupalLogout();
$this->cronRun();
// Search for the comment subject.
$edit = [
'keys' => "'" . $edit_comment['subject[0][value]'] . "'",
];
$this->drupalPostForm('search/node', $edit, t('Search'));
$node_storage->resetCache([$node->id()]);
$node2 = $node_storage->load($node->id());
$this->assertText($node2->label(), 'Node found in search results.');
$this->assertText($edit_comment['subject[0][value]'], 'Comment subject found in search results.');
// Search for the comment body.
$edit = [
'keys' => "'" . $comment_body . "'",
];
$this->drupalPostForm(NULL, $edit, t('Search'));
$this->assertText($node2->label(), 'Node found in search results.');
// Verify that comment is rendered using proper format.
$this->assertText($comment_body, 'Comment body text found in search results.');
$this->assertNoRaw(t('n/a'), 'HTML in comment body is not hidden.');
$this->assertNoEscaped($edit_comment['comment_body[0][value]'], 'HTML in comment body is not escaped.');
// Search for the evil script comment subject.
$edit = [
'keys' => 'subjectkeyword',
];
$this->drupalPostForm('search/node', $edit, t('Search'));
// Verify the evil comment subject is escaped in search results.
$this->assertRaw('&lt;script&gt;alert(&#039;<strong>subjectkeyword</strong>&#039;);');
$this->assertNoRaw('<script>');
// Search for the keyword near the evil script tag in the comment body.
$edit = [
'keys' => 'nearbykeyword',
];
$this->drupalPostForm('search/node', $edit, t('Search'));
// Verify that nearby script tag in the evil comment body is stripped from
// search results.
$this->assertRaw('<strong>nearbykeyword</strong>');
$this->assertNoRaw('<script>');
// Search for contents inside the evil script tag in the comment body.
$edit = [
'keys' => 'insidekeyword',
];
$this->drupalPostForm('search/node', $edit, t('Search'));
// @todo Verify the actual search results.
// https://www.drupal.org/node/2551135
// Verify there is no script tag in search results.
$this->assertNoRaw('<script>');
// Hide comments.
$this->drupalLogin($this->adminUser);
$node->set('comment', CommentItemInterface::HIDDEN);
$node->save();
// Invoke search index update.
$this->drupalLogout();
$this->cronRun();
// Search for $title.
$this->drupalPostForm('search/node', $edit, t('Search'));
$this->assertText(t('Your search yielded no results.'));
}
/**
* Verify access rules for comment indexing with different permissions.
*/
public function testSearchResultsCommentAccess() {
$comment_body = 'Test comment body';
$this->commentSubject = 'Test comment subject';
$roles = $this->adminUser->getRoles(TRUE);
$this->adminRole = $roles[0];
// Create a node.
// Make preview optional.
$field = FieldConfig::loadByName('node', 'article', 'comment');
$field->setSetting('preview', DRUPAL_OPTIONAL);
$field->save();
$this->node = $this->drupalCreateNode(['type' => 'article']);
// Post a comment using 'Full HTML' text format.
$edit_comment = [];
$edit_comment['subject[0][value]'] = $this->commentSubject;
$edit_comment['comment_body[0][value]'] = '<h1>' . $comment_body . '</h1>';
$this->drupalPostForm('comment/reply/node/' . $this->node->id() . '/comment', $edit_comment, t('Save'));
$this->drupalLogout();
$this->setRolePermissions(RoleInterface::ANONYMOUS_ID);
$this->assertCommentAccess(FALSE, 'Anon user has search permission but no access comments permission, comments should not be indexed');
$this->setRolePermissions(RoleInterface::ANONYMOUS_ID, TRUE);
$this->assertCommentAccess(TRUE, 'Anon user has search permission and access comments permission, comments should be indexed');
$this->drupalLogin($this->adminUser);
$this->drupalGet('admin/people/permissions');
// Disable search access for authenticated user to test admin user.
$this->setRolePermissions(RoleInterface::AUTHENTICATED_ID, FALSE, FALSE);
$this->setRolePermissions($this->adminRole);
$this->assertCommentAccess(FALSE, 'Admin user has search permission but no access comments permission, comments should not be indexed');
$this->drupalGet('node/' . $this->node->id());
$this->setRolePermissions($this->adminRole, TRUE);
$this->assertCommentAccess(TRUE, 'Admin user has search permission and access comments permission, comments should be indexed');
$this->setRolePermissions(RoleInterface::AUTHENTICATED_ID);
$this->assertCommentAccess(FALSE, 'Authenticated user has search permission but no access comments permission, comments should not be indexed');
$this->setRolePermissions(RoleInterface::AUTHENTICATED_ID, TRUE);
$this->assertCommentAccess(TRUE, 'Authenticated user has search permission and access comments permission, comments should be indexed');
// Verify that access comments permission is inherited from the
// authenticated role.
$this->setRolePermissions(RoleInterface::AUTHENTICATED_ID, TRUE, FALSE);
$this->setRolePermissions($this->adminRole);
$this->assertCommentAccess(TRUE, 'Admin user has search permission and no access comments permission, but comments should be indexed because admin user inherits authenticated user\'s permission to access comments');
// Verify that search content permission is inherited from the authenticated
// role.
$this->setRolePermissions(RoleInterface::AUTHENTICATED_ID, TRUE, TRUE);
$this->setRolePermissions($this->adminRole, TRUE, FALSE);
$this->assertCommentAccess(TRUE, 'Admin user has access comments permission and no search permission, but comments should be indexed because admin user inherits authenticated user\'s permission to search');
}
/**
* Set permissions for role.
*/
public function setRolePermissions($rid, $access_comments = FALSE, $search_content = TRUE) {
$permissions = [
'access comments' => $access_comments,
'search content' => $search_content,
];
user_role_change_permissions($rid, $permissions);
}
/**
* Update search index and search for comment.
*/
public function assertCommentAccess($assume_access, $message) {
// Invoke search index update.
search_mark_for_reindex('node_search', $this->node->id());
$this->cronRun();
// Search for the comment subject.
$edit = [
'keys' => "'" . $this->commentSubject . "'",
];
$this->drupalPostForm('search/node', $edit, t('Search'));
try {
if ($assume_access) {
$this->assertSession()->pageTextContains($this->node->label());
$this->assertSession()->pageTextContains($this->commentSubject);
}
else {
$this->assertSession()->pageTextContains(t('Your search yielded no results.'));
}
}
catch (ResponseTextException $exception) {
$this->fail($message);
}
}
/**
* Verify that 'add new comment' does not appear in search results or index.
*/
public function testAddNewComment() {
// Create a node with a short body.
$settings = [
'type' => 'article',
'title' => 'short title',
'body' => [['value' => 'short body text']],
];
$user = $this->drupalCreateUser([
'search content',
'create article content',
'access content',
'post comments',
'access comments',
]);
$this->drupalLogin($user);
$node = $this->drupalCreateNode($settings);
// Verify that if you view the node on its own page, 'add new comment'
// is there.
$this->drupalGet('node/' . $node->id());
$this->assertText(t('Add new comment'));
// Run cron to index this page.
$this->drupalLogout();
$this->cronRun();
// Search for 'comment'. Should be no results.
$this->drupalLogin($user);
$this->drupalPostForm('search/node', ['keys' => 'comment'], t('Search'));
$this->assertText(t('Your search yielded no results'));
// Search for the node title. Should be found, and 'Add new comment' should
// not be part of the search snippet.
$this->drupalPostForm('search/node', ['keys' => 'short'], t('Search'));
$this->assertText($node->label(), 'Search for keyword worked');
$this->assertNoText(t('Add new comment'));
}
}

View file

@ -0,0 +1,405 @@
<?php
namespace Drupal\Tests\search\Functional;
use Drupal\Core\Url;
use Drupal\search\Entity\SearchPage;
use Drupal\Tests\BrowserTestBase;
/**
* Verify the search config settings form.
*
* @group search
*/
class SearchConfigSettingsFormTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['block', 'dblog', 'node', 'search', 'search_extra_type', 'test_page_test'];
/**
* User who can search and administer search.
*
* @var \Drupal\user\UserInterface
*/
protected $searchUser;
/**
* Node indexed for searching.
*
* @var \Drupal\node\NodeInterface
*/
protected $searchNode;
protected function setUp() {
parent::setUp();
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
// Log in as a user that can create and search content.
$this->searchUser = $this->drupalCreateUser(['search content', 'administer search', 'administer nodes', 'bypass node access', 'access user profiles', 'administer users', 'administer blocks', 'access site reports']);
$this->drupalLogin($this->searchUser);
// Add a single piece of content and index it.
$node = $this->drupalCreateNode();
$this->searchNode = $node;
// Link the node to itself to test that it's only indexed once. The content
// also needs the word "pizza" so we can use it as the search keyword.
$body_key = 'body[0][value]';
$edit[$body_key] = \Drupal::l($node->label(), $node->urlInfo()) . ' pizza sandwich';
$this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save'));
$this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex();
search_update_totals();
// Enable the search block.
$this->drupalPlaceBlock('search_form_block');
$this->drupalPlaceBlock('local_tasks_block');
$this->drupalPlaceBlock('page_title_block');
}
/**
* Verifies the search settings form.
*/
public function testSearchSettingsPage() {
// Test that the settings form displays the correct count of items left to index.
$this->drupalGet('admin/config/search/pages');
$this->assertText(t('There are @count items left to index.', ['@count' => 0]));
// Test the re-index button.
$this->drupalPostForm('admin/config/search/pages', [], t('Re-index site'));
$this->assertText(t('Are you sure you want to re-index the site'));
$this->drupalPostForm('admin/config/search/pages/reindex', [], t('Re-index site'));
$this->assertText(t('All search indexes will be rebuilt'));
$this->drupalGet('admin/config/search/pages');
$this->assertText(t('There is 1 item left to index.'));
// Test that the form saves with the default values.
$this->drupalPostForm('admin/config/search/pages', [], t('Save configuration'));
$this->assertText(t('The configuration options have been saved.'), 'Form saves with the default values.');
// Test that the form does not save with an invalid word length.
$edit = [
'minimum_word_size' => $this->randomMachineName(3),
];
$this->drupalPostForm('admin/config/search/pages', $edit, t('Save configuration'));
$this->assertNoText(t('The configuration options have been saved.'), 'Form does not save with an invalid word length.');
// Test logging setting. It should be off by default.
$text = $this->randomMachineName(5);
$this->drupalPostForm('search/node', ['keys' => $text], t('Search'));
$this->drupalGet('admin/reports/dblog');
$this->assertNoLink('Searched Content for ' . $text . '.', 'Search was not logged');
// Turn on logging.
$edit = ['logging' => TRUE];
$this->drupalPostForm('admin/config/search/pages', $edit, t('Save configuration'));
$text = $this->randomMachineName(5);
$this->drupalPostForm('search/node', ['keys' => $text], t('Search'));
$this->drupalGet('admin/reports/dblog');
$this->assertLink('Searched Content for ' . $text . '.', 0, 'Search was logged');
}
/**
* Verifies plugin-supplied settings form.
*/
public function testSearchModuleSettingsPage() {
$this->drupalGet('admin/config/search/pages');
$this->clickLink(t('Edit'), 1);
// Ensure that the default setting was picked up from the default config
$this->assertTrue($this->xpath('//select[@id="edit-extra-type-settings-boost"]//option[@value="bi" and @selected="selected"]'), 'Module specific settings are picked up from the default config');
// Change extra type setting and also modify a common search setting.
$edit = [
'extra_type_settings[boost]' => 'ii',
];
$this->drupalPostForm(NULL, $edit, t('Save search page'));
// Ensure that the modifications took effect.
$this->assertRaw(t('The %label search page has been updated.', ['%label' => 'Dummy search type']));
$this->drupalGet('admin/config/search/pages/manage/dummy_search_type');
$this->assertTrue($this->xpath('//select[@id="edit-extra-type-settings-boost"]//option[@value="ii" and @selected="selected"]'), 'Module specific settings can be changed');
}
/**
* Verifies that you can disable individual search plugins.
*/
public function testSearchModuleDisabling() {
// Array of search plugins to test: 'keys' are the keywords to search for,
// and 'text' is the text to assert is on the results page.
$plugin_info = [
'node_search' => [
'keys' => 'pizza',
'text' => $this->searchNode->label(),
],
'user_search' => [
'keys' => $this->searchUser->getUsername(),
'text' => $this->searchUser->getEmail(),
],
'dummy_search_type' => [
'keys' => 'foo',
'text' => 'Dummy search snippet to display',
],
];
$plugins = array_keys($plugin_info);
/** @var $entities \Drupal\search\SearchPageInterface[] */
$entities = SearchPage::loadMultiple();
// Disable all of the search pages.
foreach ($entities as $entity) {
$entity->disable()->save();
}
// Test each plugin if it's enabled as the only search plugin.
foreach ($entities as $entity_id => $entity) {
$this->setDefaultThroughUi($entity_id);
// Run a search from the correct search URL.
$info = $plugin_info[$entity_id];
$this->drupalGet('search/' . $entity->getPath(), ['query' => ['keys' => $info['keys']]]);
$this->assertResponse(200);
$this->assertNoText('no results', $entity->label() . ' search found results');
$this->assertText($info['text'], 'Correct search text found');
// Verify that other plugin search tab labels are not visible.
foreach ($plugins as $other) {
if ($other != $entity_id) {
$label = $entities[$other]->label();
$this->assertNoText($label, $label . ' search tab is not shown');
}
}
// Run a search from the search block on the node page. Verify you get
// to this plugin's search results page.
$terms = ['keys' => $info['keys']];
$this->drupalPostForm('node', $terms, t('Search'));
$current = $this->getURL();
$expected = \Drupal::url('search.view_' . $entity->id(), [], ['query' => ['keys' => $info['keys']], 'absolute' => TRUE]);
$this->assertEqual($current, $expected, 'Block redirected to right search page');
// Try an invalid search path, which should 404.
$this->drupalGet('search/not_a_plugin_path');
$this->assertResponse(404);
$entity->disable()->save();
}
// Set the node search as default.
$this->setDefaultThroughUi('node_search');
// Test with all search plugins enabled. When you go to the search
// page or run search, all plugins should be shown.
foreach ($entities as $entity) {
$entity->enable()->save();
}
\Drupal::service('router.builder')->rebuild();
$paths = [
['path' => 'search/node', 'options' => ['query' => ['keys' => 'pizza']]],
['path' => 'search/node', 'options' => []],
];
foreach ($paths as $item) {
$this->drupalGet($item['path'], $item['options']);
foreach ($plugins as $entity_id) {
$label = $entities[$entity_id]->label();
$this->assertText($label, format_string('%label search tab is shown', ['%label' => $label]));
}
}
}
/**
* Tests the ordering of search pages on a clean install.
*/
public function testDefaultSearchPageOrdering() {
$this->drupalGet('search');
$elements = $this->xpath('//*[contains(@class, :class)]//a', [':class' => 'tabs primary']);
$this->assertIdentical($elements[0]->getAttribute('href'), \Drupal::url('search.view_node_search'));
$this->assertIdentical($elements[1]->getAttribute('href'), \Drupal::url('search.view_dummy_search_type'));
$this->assertIdentical($elements[2]->getAttribute('href'), \Drupal::url('search.view_user_search'));
}
/**
* Tests multiple search pages of the same type.
*/
public function testMultipleSearchPages() {
$this->assertDefaultSearch('node_search', 'The default page is set to the installer default.');
$search_storage = \Drupal::entityManager()->getStorage('search_page');
$entities = $search_storage->loadMultiple();
$search_storage->delete($entities);
$this->assertDefaultSearch(FALSE);
// Ensure that no search pages are configured.
$this->drupalGet('admin/config/search/pages');
$this->assertText(t('No search pages have been configured.'));
// Add a search page.
$edit = [];
$edit['search_type'] = 'search_extra_type_search';
$this->drupalPostForm(NULL, $edit, t('Add search page'));
$this->assertTitle('Add new search page | Drupal');
$first = [];
$first['label'] = $this->randomString();
$first_id = $first['id'] = strtolower($this->randomMachineName(8));
$first['path'] = strtolower($this->randomMachineName(8));
$this->drupalPostForm(NULL, $first, t('Save'));
$this->assertDefaultSearch($first_id, 'The default page matches the only search page.');
$this->assertRaw(t('The %label search page has been added.', ['%label' => $first['label']]));
// Attempt to add a search page with an existing path.
$edit = [];
$edit['search_type'] = 'search_extra_type_search';
$this->drupalPostForm(NULL, $edit, t('Add search page'));
$edit = [];
$edit['label'] = $this->randomString();
$edit['id'] = strtolower($this->randomMachineName(8));
$edit['path'] = $first['path'];
$this->drupalPostForm(NULL, $edit, t('Save'));
$this->assertText(t('The search page path must be unique.'));
// Add a second search page.
$second = [];
$second['label'] = $this->randomString();
$second_id = $second['id'] = strtolower($this->randomMachineName(8));
$second['path'] = strtolower($this->randomMachineName(8));
$this->drupalPostForm(NULL, $second, t('Save'));
$this->assertDefaultSearch($first_id, 'The default page matches the only search page.');
// Ensure both search pages have their tabs displayed.
$this->drupalGet('search');
$elements = $this->xpath('//*[contains(@class, :class)]//a', [':class' => 'tabs primary']);
$this->assertIdentical($elements[0]->getAttribute('href'), Url::fromRoute('search.view_' . $first_id)->toString());
$this->assertIdentical($elements[1]->getAttribute('href'), Url::fromRoute('search.view_' . $second_id)->toString());
// Switch the weight of the search pages and check the order of the tabs.
$edit = [
'entities[' . $first_id . '][weight]' => 10,
'entities[' . $second_id . '][weight]' => -10,
];
$this->drupalPostForm('admin/config/search/pages', $edit, t('Save configuration'));
$this->drupalGet('search');
$elements = $this->xpath('//*[contains(@class, :class)]//a', [':class' => 'tabs primary']);
$this->assertIdentical($elements[0]->getAttribute('href'), Url::fromRoute('search.view_' . $second_id)->toString());
$this->assertIdentical($elements[1]->getAttribute('href'), Url::fromRoute('search.view_' . $first_id)->toString());
// Check the initial state of the search pages.
$this->drupalGet('admin/config/search/pages');
$this->verifySearchPageOperations($first_id, TRUE, FALSE, FALSE, FALSE);
$this->verifySearchPageOperations($second_id, TRUE, TRUE, TRUE, FALSE);
// Change the default search page.
$this->clickLink(t('Set as default'));
$this->assertRaw(t('The default search page is now %label. Be sure to check the ordering of your search pages.', ['%label' => $second['label']]));
$this->verifySearchPageOperations($first_id, TRUE, TRUE, TRUE, FALSE);
$this->verifySearchPageOperations($second_id, TRUE, FALSE, FALSE, FALSE);
// Disable the first search page.
$this->clickLink(t('Disable'));
$this->assertResponse(200);
$this->assertNoLink(t('Disable'));
$this->verifySearchPageOperations($first_id, TRUE, TRUE, FALSE, TRUE);
$this->verifySearchPageOperations($second_id, TRUE, FALSE, FALSE, FALSE);
// Enable the first search page.
$this->clickLink(t('Enable'));
$this->assertResponse(200);
$this->verifySearchPageOperations($first_id, TRUE, TRUE, TRUE, FALSE);
$this->verifySearchPageOperations($second_id, TRUE, FALSE, FALSE, FALSE);
// Test deleting.
$this->clickLink(t('Delete'));
$this->assertRaw(t('Are you sure you want to delete the search page %label?', ['%label' => $first['label']]));
$this->drupalPostForm(NULL, [], t('Delete'));
$this->assertRaw(t('The search page %label has been deleted.', ['%label' => $first['label']]));
$this->verifySearchPageOperations($first_id, FALSE, FALSE, FALSE, FALSE);
}
/**
* Tests that the enable/disable/default routes are protected from CSRF.
*/
public function testRouteProtection() {
// Ensure that the enable and disable routes are protected.
$this->drupalGet('admin/config/search/pages/manage/node_search/enable');
$this->assertResponse(403);
$this->drupalGet('admin/config/search/pages/manage/node_search/disable');
$this->assertResponse(403);
$this->drupalGet('admin/config/search/pages/manage/node_search/set-default');
$this->assertResponse(403);
}
/**
* Checks that the search page operations match expectations.
*
* @param string $id
* The search page ID to check.
* @param bool $edit
* Whether the edit link is expected.
* @param bool $delete
* Whether the delete link is expected.
* @param bool $disable
* Whether the disable link is expected.
* @param bool $enable
* Whether the enable link is expected.
*/
protected function verifySearchPageOperations($id, $edit, $delete, $disable, $enable) {
if ($edit) {
$this->assertLinkByHref("admin/config/search/pages/manage/$id");
}
else {
$this->assertNoLinkByHref("admin/config/search/pages/manage/$id");
}
if ($delete) {
$this->assertLinkByHref("admin/config/search/pages/manage/$id/delete");
}
else {
$this->assertNoLinkByHref("admin/config/search/pages/manage/$id/delete");
}
if ($disable) {
$this->assertLinkByHref("admin/config/search/pages/manage/$id/disable");
}
else {
$this->assertNoLinkByHref("admin/config/search/pages/manage/$id/disable");
}
if ($enable) {
$this->assertLinkByHref("admin/config/search/pages/manage/$id/enable");
}
else {
$this->assertNoLinkByHref("admin/config/search/pages/manage/$id/enable");
}
}
/**
* Checks that the default search page matches expectations.
*
* @param string $expected
* The expected search page.
* @param string $message
* (optional) A message to display with the assertion.
* @param string $group
* (optional) The group this message is in.
*/
protected function assertDefaultSearch($expected, $message = '', $group = 'Other') {
/** @var $search_page_repository \Drupal\search\SearchPageRepositoryInterface */
$search_page_repository = \Drupal::service('search.search_page_repository');
$this->assertIdentical($search_page_repository->getDefaultSearchPage(), $expected, $message, $group);
}
/**
* Sets a search page as the default in the UI.
*
* @param string $entity_id
* The search page entity ID to enable.
*/
protected function setDefaultThroughUi($entity_id) {
$this->drupalGet('admin/config/search/pages');
preg_match('|href="([^"]+' . $entity_id . '/set-default[^"]+)"|', $this->getSession()->getPage()->getContent(), $matches);
$this->drupalGet($this->getAbsoluteUrl($matches[1]));
}
}

View file

@ -0,0 +1,76 @@
<?php
namespace Drupal\Tests\search\Functional;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\Tests\BrowserTestBase;
/**
* Tests searching with date filters that exclude some translations.
*
* @group search
*/
class SearchDateIntervalTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['language', 'search_date_query_alter', 'node', 'search'];
protected function setUp() {
parent::setUp();
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
// Create and log in user.
$test_user = $this->drupalCreateUser(['access content', 'search content', 'use advanced search', 'administer nodes', 'administer languages', 'access administration pages', 'administer site configuration']);
$this->drupalLogin($test_user);
// Add a new language.
ConfigurableLanguage::createFromLangcode('es')->save();
// Set up times to be applied to the English and Spanish translations of the
// node create time, so that they are filtered in/out in the
// search_date_query_alter test module.
$created_time_en = new \DateTime('February 10 2016 10PM');
$created_time_es = new \DateTime('March 19 2016 10PM');
$default_format = filter_default_format();
$node = $this->drupalCreateNode([
'title' => 'Node EN',
'type' => 'page',
'body' => [
'value' => $this->randomMachineName(32),
'format' => $default_format,
],
'langcode' => 'en',
'created' => $created_time_en->getTimestamp(),
]);
// Add Spanish translation to the node.
$translation = $node->addTranslation('es', ['title' => 'Node ES']);
$translation->body->value = $this->randomMachineName(32);
$translation->created->value = $created_time_es->getTimestamp();
$node->save();
// Update the index.
$plugin = $this->container->get('plugin.manager.search')->createInstance('node_search');
$plugin->updateIndex();
search_update_totals();
}
/**
* Tests searching with date filters that exclude some translations.
*/
public function testDateIntervalQueryAlter() {
// Search for keyword node.
$edit = ['keys' => 'node'];
$this->drupalPostForm('search/node', $edit, t('Search'));
// The nodes must have the same node ID but the created date is different.
// So only the Spanish translation must appear.
$this->assertLink('Node ES', 0, 'Spanish translation found in search results');
$this->assertNoLink('Node EN', 'Search results do not contain English node');
}
}

View file

@ -0,0 +1,87 @@
<?php
namespace Drupal\Tests\search\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Verifies that a form embedded in search results works.
*
* @group search
*/
class SearchEmbedFormTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['node', 'search', 'search_embedded_form'];
/**
* Node used for testing.
*
* @var \Drupal\node\NodeInterface
*/
protected $node;
/**
* Count of how many times the form has been submitted.
*
* @var int
*/
protected $submitCount = 0;
protected function setUp() {
parent::setUp();
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
// Create a user and a node, and update the search index.
$test_user = $this->drupalCreateUser(['access content', 'search content', 'administer nodes']);
$this->drupalLogin($test_user);
$this->node = $this->drupalCreateNode();
$this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex();
search_update_totals();
// Set up a dummy initial count of times the form has been submitted.
$this->submitCount = \Drupal::state()->get('search_embedded_form.submit_count');
$this->refreshVariables();
}
/**
* Tests that the embedded form appears and can be submitted.
*/
public function testEmbeddedForm() {
// First verify we can submit the form from the module's page.
$this->drupalPostForm('search_embedded_form',
['name' => 'John'],
t('Send away'));
$this->assertText(t('Test form was submitted'), 'Form message appears');
$count = \Drupal::state()->get('search_embedded_form.submit_count');
$this->assertEqual($this->submitCount + 1, $count, 'Form submission count is correct');
$this->submitCount = $count;
// Now verify that we can see and submit the form from the search results.
$this->drupalGet('search/node', ['query' => ['keys' => $this->node->label()]]);
$this->assertText(t('Your name'), 'Form is visible');
$this->drupalPostForm(NULL,
['name' => 'John'],
t('Send away'));
$this->assertText(t('Test form was submitted'), 'Form message appears');
$count = \Drupal::state()->get('search_embedded_form.submit_count');
$this->assertEqual($this->submitCount + 1, $count, 'Form submission count is correct');
$this->submitCount = $count;
// Now verify that if we submit the search form, it doesn't count as
// our form being submitted.
$this->drupalPostForm('search',
['keys' => 'foo'],
t('Search'));
$this->assertNoText(t('Test form was submitted'), 'Form message does not appear');
$count = \Drupal::state()->get('search_embedded_form.submit_count');
$this->assertEqual($this->submitCount, $count, 'Form submission count is correct');
$this->submitCount = $count;
}
}

View file

@ -0,0 +1,89 @@
<?php
namespace Drupal\Tests\search\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests that searching for a phrase gets the correct page count.
*
* @group search
*/
class SearchExactTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['node', 'search'];
/**
* Tests that the correct number of pager links are found for both keywords and phrases.
*/
public function testExactQuery() {
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
// Log in with sufficient privileges.
$user = $this->drupalCreateUser(['create page content', 'search content']);
$this->drupalLogin($user);
$settings = [
'type' => 'page',
'title' => 'Simple Node',
];
// Create nodes with exact phrase.
for ($i = 0; $i <= 17; $i++) {
$settings['body'] = [['value' => 'love pizza']];
$this->drupalCreateNode($settings);
}
// Create nodes containing keywords.
for ($i = 0; $i <= 17; $i++) {
$settings['body'] = [['value' => 'love cheesy pizza']];
$this->drupalCreateNode($settings);
}
// Create another node and save it for later.
$settings['body'] = [['value' => 'Druplicon']];
$node = $this->drupalCreateNode($settings);
// Update the search index.
$this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex();
search_update_totals();
// Refresh variables after the treatment.
$this->refreshVariables();
// Test that the correct number of pager links are found for keyword search.
$edit = ['keys' => 'love pizza'];
$this->drupalPostForm('search/node', $edit, t('Search'));
$this->assertLinkByHref('page=1', 0, '2nd page link is found for keyword search.');
$this->assertLinkByHref('page=2', 0, '3rd page link is found for keyword search.');
$this->assertLinkByHref('page=3', 0, '4th page link is found for keyword search.');
$this->assertNoLinkByHref('page=4', '5th page link is not found for keyword search.');
// Test that the correct number of pager links are found for exact phrase search.
$edit = ['keys' => '"love pizza"'];
$this->drupalPostForm('search/node', $edit, t('Search'));
$this->assertLinkByHref('page=1', 0, '2nd page link is found for exact phrase search.');
$this->assertNoLinkByHref('page=2', '3rd page link is not found for exact phrase search.');
// Check that with post settings turned on the post information is displayed.
$node_type_config = \Drupal::configFactory()->getEditable('node.type.page');
$node_type_config->set('display_submitted', TRUE);
$node_type_config->save();
$edit = ['keys' => 'Druplicon'];
$this->drupalPostForm('search/node', $edit, t('Search'));
$this->assertText($user->getUsername(), 'Basic page node displays author name when post settings are on.');
$this->assertText(format_date($node->getChangedTime(), 'short'), 'Basic page node displays post date when post settings are on.');
// Check that with post settings turned off the user and changed date
// information is not displayed.
$node_type_config->set('display_submitted', FALSE);
$node_type_config->save();
$edit = ['keys' => 'Druplicon'];
$this->drupalPostForm('search/node', $edit, t('Search'));
$this->assertNoText($user->getUsername(), 'Basic page node does not display author name when post settings are off.');
$this->assertNoText(format_date($node->getChangedTime(), 'short'), 'Basic page node does not display post date when post settings are off.');
}
}

View file

@ -0,0 +1,61 @@
<?php
namespace Drupal\Tests\search\Functional;
use Drupal\Component\Utility\Html;
use Drupal\Tests\BrowserTestBase;
/**
* Verify the search without keywords set and extra conditions.
*
* Verifies that a plugin can override the isSearchExecutable() method to allow
* searching without keywords set and that GET query parameters are made
* available to plugins during search execution.
*
* @group search
*/
class SearchKeywordsConditionsTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['comment', 'search', 'search_extra_type', 'test_page_test'];
/**
* A user with permission to search and post comments.
*
* @var \Drupal\user\UserInterface
*/
protected $searchingUser;
protected function setUp() {
parent::setUp();
// Create searching user.
$this->searchingUser = $this->drupalCreateUser(['search content', 'access content', 'access comments', 'skip comment approval']);
// Log in with sufficient privileges.
$this->drupalLogin($this->searchingUser);
}
/**
* Verify the keywords are captured and conditions respected.
*/
public function testSearchKeywordsConditions() {
// No keys, not conditions - no results.
$this->drupalGet('search/dummy_path');
$this->assertNoText('Dummy search snippet to display');
// With keys - get results.
$keys = 'bike shed ' . $this->randomMachineName();
$this->drupalGet("search/dummy_path", ['query' => ['keys' => $keys]]);
$this->assertText("Dummy search snippet to display. Keywords: {$keys}");
$keys = 'blue drop ' . $this->randomMachineName();
$this->drupalGet("search/dummy_path", ['query' => ['keys' => $keys]]);
$this->assertText("Dummy search snippet to display. Keywords: {$keys}");
// Add some conditions and keys.
$keys = 'moving drop ' . $this->randomMachineName();
$this->drupalGet("search/dummy_path", ['query' => ['keys' => 'bike', 'search_conditions' => $keys]]);
$this->assertText("Dummy search snippet to display.");
$this->assertRaw(Html::escape(print_r(['keys' => 'bike', 'search_conditions' => $keys], TRUE)));
}
}

View file

@ -0,0 +1,138 @@
<?php
namespace Drupal\Tests\search\Functional;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\Tests\BrowserTestBase;
/**
* Tests advanced search with different languages added.
*
* @group search
*/
class SearchLanguageTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['language', 'node', 'search'];
/**
* Array of nodes available to search.
*
* @var \Drupal\node\NodeInterface[]
*/
protected $searchableNodes;
protected function setUp() {
parent::setUp();
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
// Create and log in user.
$test_user = $this->drupalCreateUser(['access content', 'search content', 'use advanced search', 'administer nodes', 'administer languages', 'access administration pages', 'administer site configuration']);
$this->drupalLogin($test_user);
// Add a new language.
ConfigurableLanguage::createFromLangcode('es')->save();
// Make the body field translatable. The title is already translatable by
// definition. The parent class has already created the article and page
// content types.
$field_storage = FieldStorageConfig::loadByName('node', 'body');
$field_storage->setTranslatable(TRUE);
$field_storage->save();
// Create a few page nodes with multilingual body values.
$default_format = filter_default_format();
$nodes = [
[
'title' => 'First node en',
'type' => 'page',
'body' => [['value' => $this->randomMachineName(32), 'format' => $default_format]],
'langcode' => 'en',
],
[
'title' => 'Second node this is the Spanish title',
'type' => 'page',
'body' => [['value' => $this->randomMachineName(32), 'format' => $default_format]],
'langcode' => 'es',
],
[
'title' => 'Third node en',
'type' => 'page',
'body' => [['value' => $this->randomMachineName(32), 'format' => $default_format]],
'langcode' => 'en',
],
];
$this->searchableNodes = [];
foreach ($nodes as $setting) {
$this->searchableNodes[] = $this->drupalCreateNode($setting);
}
// Add English translation to the second node.
$translation = $this->searchableNodes[1]->addTranslation('en', ['title' => 'Second node en']);
$translation->body->value = $this->randomMachineName(32);
$this->searchableNodes[1]->save();
// Add Spanish translation to the third node.
$translation = $this->searchableNodes[2]->addTranslation('es', ['title' => 'Third node es']);
$translation->body->value = $this->randomMachineName(32);
$this->searchableNodes[2]->save();
// Update the index and then run the shutdown method.
$plugin = $this->container->get('plugin.manager.search')->createInstance('node_search');
$plugin->updateIndex();
search_update_totals();
}
public function testLanguages() {
// Add predefined language.
$edit = ['predefined_langcode' => 'fr'];
$this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language'));
$this->assertText('French', 'Language added successfully.');
// Now we should have languages displayed.
$this->drupalGet('search/node');
$this->assertText(t('Languages'), 'Languages displayed to choose from.');
$this->assertText(t('English'), 'English is a possible choice.');
$this->assertText(t('French'), 'French is a possible choice.');
// Ensure selecting no language does not make the query different.
$this->drupalPostForm('search/node', [], 'edit-submit--2');
$this->assertUrl(\Drupal::url('search.view_node_search', [], ['query' => ['keys' => ''], 'absolute' => TRUE]), [], 'Correct page redirection, no language filtering.');
// Pick French and ensure it is selected.
$edit = ['language[fr]' => TRUE];
$this->drupalPostForm('search/node', $edit, 'edit-submit--2');
// Get the redirected URL.
$url = $this->getUrl();
$parts = parse_url($url);
$query_string = isset($parts['query']) ? rawurldecode($parts['query']) : '';
$this->assertTrue(strpos($query_string, '=language:fr') !== FALSE, 'Language filter language:fr add to the query string.');
// Search for keyword node and language filter as Spanish.
$edit = ['keys' => 'node', 'language[es]' => TRUE];
$this->drupalPostForm('search/node', $edit, 'edit-submit--2');
// Check for Spanish results.
$this->assertLink('Second node this is the Spanish title', 0, 'Second node Spanish title found in search results');
$this->assertLink('Third node es', 0, 'Third node Spanish found in search results');
// Ensure that results don't contain other language nodes.
$this->assertNoLink('First node en', 'Search results do not contain first English node');
$this->assertNoLink('Second node en', 'Search results do not contain second English node');
$this->assertNoLink('Third node en', 'Search results do not contain third English node');
// Change the default language and delete English.
$path = 'admin/config/regional/language';
$this->drupalGet($path);
$this->assertFieldChecked('edit-site-default-language-en', 'Default language updated.');
$edit = [
'site_default_language' => 'fr',
];
$this->drupalPostForm($path, $edit, t('Save configuration'));
$this->assertNoFieldChecked('edit-site-default-language-en', 'Default language updated.');
$this->drupalPostForm('admin/config/regional/language/delete/en', [], t('Delete'));
}
}

View file

@ -0,0 +1,323 @@
<?php
namespace Drupal\Tests\search\Functional;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\Tests\BrowserTestBase;
/**
* Tests entities with multilingual fields.
*
* @group search
*/
class SearchMultilingualEntityTest extends BrowserTestBase {
/**
* List of searchable nodes.
*
* @var \Drupal\node\NodeInterface[]
*/
protected $searchableNodes = [];
/**
* Node search plugin.
*
* @var \Drupal\node\Plugin\Search\NodeSearch
*/
protected $plugin;
/**
* {@inheritdoc}
*/
protected static $modules = ['language', 'locale', 'comment', 'node', 'search'];
protected function setUp() {
parent::setUp();
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
// Create a user who can administer search, do searches, see the status
// report, and administer cron. Log in.
$user = $this->drupalCreateUser(['administer search', 'search content', 'use advanced search', 'access content', 'access site reports', 'administer site configuration']);
$this->drupalLogin($user);
// Set up the search plugin.
$this->plugin = $this->container->get('plugin.manager.search')->createInstance('node_search');
// Check indexing counts before adding any nodes.
$this->assertIndexCounts(0, 0, 'before adding nodes');
$this->assertDatabaseCounts(0, 0, 'before adding nodes');
// Add two new languages.
ConfigurableLanguage::createFromLangcode('hu')->save();
ConfigurableLanguage::createFromLangcode('sv')->save();
// Make the body field translatable. The title is already translatable by
// definition. The parent class has already created the article and page
// content types.
$field_storage = FieldStorageConfig::loadByName('node', 'body');
$field_storage->setTranslatable(TRUE);
$field_storage->save();
// Create a few page nodes with multilingual body values.
$default_format = filter_default_format();
$nodes = [
[
'title' => 'First node en',
'type' => 'page',
'body' => [['value' => $this->randomMachineName(32), 'format' => $default_format]],
'langcode' => 'en',
],
[
'title' => 'Second node this is the English title',
'type' => 'page',
'body' => [['value' => $this->randomMachineName(32), 'format' => $default_format]],
'langcode' => 'en',
],
[
'title' => 'Third node en',
'type' => 'page',
'body' => [['value' => $this->randomMachineName(32), 'format' => $default_format]],
'langcode' => 'en',
],
// After the third node, we don't care what the settings are. But we
// need to have at least 5 to make sure the throttling is working
// correctly. So, let's make 8 total.
[],
[],
[],
[],
[],
];
$this->searchableNodes = [];
foreach ($nodes as $setting) {
$this->searchableNodes[] = $this->drupalCreateNode($setting);
}
// Add a single translation to the second node.
$translation = $this->searchableNodes[1]->addTranslation('hu', ['title' => 'Second node hu']);
$translation->body->value = $this->randomMachineName(32);
$this->searchableNodes[1]->save();
// Add two translations to the third node.
$translation = $this->searchableNodes[2]->addTranslation('hu', ['title' => 'Third node this is the Hungarian title']);
$translation->body->value = $this->randomMachineName(32);
$translation = $this->searchableNodes[2]->addTranslation('sv', ['title' => 'Third node sv']);
$translation->body->value = $this->randomMachineName(32);
$this->searchableNodes[2]->save();
// Verify that we have 8 nodes left to do.
$this->assertIndexCounts(8, 8, 'before updating the search index');
$this->assertDatabaseCounts(0, 0, 'before updating the search index');
}
/**
* Tests the indexing throttle and search results with multilingual nodes.
*/
public function testMultilingualSearch() {
// Index only 2 nodes per cron run. We cannot do this setting in the UI,
// because it doesn't go this low.
$this->config('search.settings')->set('index.cron_limit', 2)->save();
// Get a new search plugin, to make sure it has this setting.
$this->plugin = $this->container->get('plugin.manager.search')->createInstance('node_search');
// Update the index. This does the initial processing.
$this->plugin->updateIndex();
// Run the shutdown function. Testing is a unique case where indexing
// and searching has to happen in the same request, so running the shutdown
// function manually is needed to finish the indexing process.
search_update_totals();
$this->assertIndexCounts(6, 8, 'after updating partially');
$this->assertDatabaseCounts(2, 0, 'after updating partially');
// Now index the rest of the nodes.
// Make sure index throttle is high enough, via the UI.
$this->drupalPostForm('admin/config/search/pages', ['cron_limit' => 20], t('Save configuration'));
$this->assertEqual(20, $this->config('search.settings')->get('index.cron_limit', 100), 'Config setting was saved correctly');
// Get a new search plugin, to make sure it has this setting.
$this->plugin = $this->container->get('plugin.manager.search')->createInstance('node_search');
$this->plugin->updateIndex();
search_update_totals();
$this->assertIndexCounts(0, 8, 'after updating fully');
$this->assertDatabaseCounts(8, 0, 'after updating fully');
// Click the reindex button on the admin page, verify counts, and reindex.
$this->drupalPostForm('admin/config/search/pages', [], t('Re-index site'));
$this->drupalPostForm(NULL, [], t('Re-index site'));
$this->assertIndexCounts(8, 8, 'after reindex');
$this->assertDatabaseCounts(8, 0, 'after reindex');
$this->plugin->updateIndex();
search_update_totals();
// Test search results.
// This should find two results for the second and third node.
$this->plugin->setSearch('English OR Hungarian', [], []);
$search_result = $this->plugin->execute();
$this->assertEqual(count($search_result), 2, 'Found two results.');
// Nodes are saved directly after each other and have the same created time
// so testing for the order is not possible.
$results = [$search_result[0]['title'], $search_result[1]['title']];
$this->assertTrue(in_array('Third node this is the Hungarian title', $results), 'The search finds the correct Hungarian title.');
$this->assertTrue(in_array('Second node this is the English title', $results), 'The search finds the correct English title.');
// Now filter for Hungarian results only.
$this->plugin->setSearch('English OR Hungarian', ['f' => ['language:hu']], []);
$search_result = $this->plugin->execute();
$this->assertEqual(count($search_result), 1, 'The search found only one result');
$this->assertEqual($search_result[0]['title'], 'Third node this is the Hungarian title', 'The search finds the correct Hungarian title.');
// Test for search with common key word across multiple languages.
$this->plugin->setSearch('node', [], []);
$search_result = $this->plugin->execute();
$this->assertEqual(count($search_result), 6, 'The search found total six results');
// Test with language filters and common key word.
$this->plugin->setSearch('node', ['f' => ['language:hu']], []);
$search_result = $this->plugin->execute();
$this->assertEqual(count($search_result), 2, 'The search found 2 results');
// Test to check for the language of result items.
foreach ($search_result as $result) {
$this->assertEqual($result['langcode'], 'hu', 'The search found the correct Hungarian result');
}
// Mark one of the nodes for reindexing, using the API function, and
// verify indexing status.
search_mark_for_reindex('node_search', $this->searchableNodes[0]->id());
$this->assertIndexCounts(1, 8, 'after marking one node to reindex via API function');
// Update the index and verify the totals again.
$this->plugin = $this->container->get('plugin.manager.search')->createInstance('node_search');
$this->plugin->updateIndex();
search_update_totals();
$this->assertIndexCounts(0, 8, 'after indexing again');
// Mark one node for reindexing by saving it, and verify indexing status.
$this->searchableNodes[1]->save();
$this->assertIndexCounts(1, 8, 'after marking one node to reindex via save');
// The request time is always the same throughout test runs. Update the
// request time to a previous time, to simulate it having been marked
// previously.
$current = REQUEST_TIME;
$old = $current - 10;
db_update('search_dataset')
->fields(['reindex' => $old])
->condition('reindex', $current, '>=')
->execute();
// Save the node again. Verify that the request time on it is not updated.
$this->searchableNodes[1]->save();
$result = db_select('search_dataset', 'd')
->fields('d', ['reindex'])
->condition('type', 'node_search')
->condition('sid', $this->searchableNodes[1]->id())
->execute()
->fetchField();
$this->assertEqual($result, $old, 'Reindex time was not updated if node was already marked');
// Add a bogus entry to the search index table using a different search
// type. This will not appear in the index status, because it is not
// managed by a plugin.
search_index('foo', $this->searchableNodes[0]->id(), 'en', 'some text');
$this->assertIndexCounts(1, 8, 'after adding a different index item');
// Mark just this "foo" index for reindexing.
search_mark_for_reindex('foo');
$this->assertIndexCounts(1, 8, 'after reindexing the other search type');
// Mark everything for reindexing.
search_mark_for_reindex();
$this->assertIndexCounts(8, 8, 'after reindexing everything');
// Clear one item from the index, but with wrong language.
$this->assertDatabaseCounts(8, 1, 'before clear');
search_index_clear('node_search', $this->searchableNodes[0]->id(), 'hu');
$this->assertDatabaseCounts(8, 1, 'after clear with wrong language');
// Clear using correct language.
search_index_clear('node_search', $this->searchableNodes[0]->id(), 'en');
$this->assertDatabaseCounts(7, 1, 'after clear with right language');
// Don't specify language.
search_index_clear('node_search', $this->searchableNodes[1]->id());
$this->assertDatabaseCounts(6, 1, 'unspecified language clear');
// Clear everything in 'foo'.
search_index_clear('foo');
$this->assertDatabaseCounts(6, 0, 'other index clear');
// Clear everything.
search_index_clear();
$this->assertDatabaseCounts(0, 0, 'complete clear');
}
/**
* Verifies the indexing status counts.
*
* @param int $remaining
* Count of remaining items to verify.
* @param int $total
* Count of total items to verify.
* @param string $message
* Message to use, something like "after updating the search index".
*/
protected function assertIndexCounts($remaining, $total, $message) {
// Check status via plugin method call.
$status = $this->plugin->indexStatus();
$this->assertEqual($status['remaining'], $remaining, 'Remaining items ' . $message . ' is ' . $remaining);
$this->assertEqual($status['total'], $total, 'Total items ' . $message . ' is ' . $total);
// Check text in progress section of Search settings page. Note that this
// test avoids using
// \Drupal\Core\StringTranslation\TranslationInterface::formatPlural(), so
// it tests for fragments of text.
$indexed = $total - $remaining;
$percent = ($total > 0) ? floor(100 * $indexed / $total) : 100;
$this->drupalGet('admin/config/search/pages');
$this->assertText($percent . '% of the site has been indexed.', 'Progress percent text at top of Search settings page is correct at: ' . $message);
$this->assertText($remaining . ' item', 'Remaining text at top of Search settings page is correct at: ' . $message);
// Check text in pages section of Search settings page.
$this->assertText($indexed . ' of ' . $total . ' indexed', 'Progress text in pages section of Search settings page is correct at: ' . $message);
// Check text on status report page.
$this->drupalGet('admin/reports/status');
$this->assertText('Search index progress', 'Search status section header is present on status report page');
$this->assertText($percent . '%', 'Correct percentage is shown on status report page at: ' . $message);
$this->assertText('(' . $remaining . ' remaining)', 'Correct remaining value is shown on status report page at: ' . $message);
}
/**
* Checks actual database counts of items in the search index.
*
* @param int $count_node
* Count of node items to assert.
* @param int $count_foo
* Count of "foo" items to assert.
* @param string $message
* Message suffix to use.
*/
protected function assertDatabaseCounts($count_node, $count_foo, $message) {
// Count number of distinct nodes by ID.
$results = db_select('search_dataset', 'i')
->fields('i', ['sid'])
->condition('type', 'node_search')
->groupBy('sid')
->execute()
->fetchCol();
$this->assertEqual($count_node, count($results), 'Node count was ' . $count_node . ' for ' . $message);
// Count number of "foo" records.
$results = db_select('search_dataset', 'i')
->fields('i', ['sid'])
->condition('type', 'foo')
->execute()
->fetchCol();
$this->assertEqual($count_foo, count($results), 'Foo count was ' . $count_foo . ' for ' . $message);
}
}

View file

@ -0,0 +1,90 @@
<?php
namespace Drupal\Tests\search\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests search functionality with diacritics.
*
* @group search
*/
class SearchNodeDiacriticsTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['node', 'search'];
/**
* A user with permission to use advanced search.
*
* @var \Drupal\user\UserInterface
*/
public $testUser;
protected function setUp() {
parent::setUp();
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
node_access_rebuild();
// Create a test user and log in.
$this->testUser = $this->drupalCreateUser(['access content', 'search content', 'use advanced search', 'access user profiles']);
$this->drupalLogin($this->testUser);
}
/**
* Tests that search returns results with diacritics in the search phrase.
*/
public function testPhraseSearchPunctuation() {
$body_text = 'The Enricþment Center is cómmīŦŧęđ to the well BɆĬŇĜ of æll påŔťıçȉpǎǹţș. ';
$body_text .= 'Also meklēt (see #731298)';
$this->drupalCreateNode(['body' => [['value' => $body_text]]]);
// Update the search index.
$this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex();
search_update_totals();
// Refresh variables after the treatment.
$this->refreshVariables();
$edit = ['keys' => 'meklet'];
$this->drupalPostForm('search/node', $edit, t('Search'));
$this->assertRaw('<strong>meklēt</strong>');
$edit = ['keys' => 'meklēt'];
$this->drupalPostForm('search/node', $edit, t('Search'));
$this->assertRaw('<strong>meklēt</strong>');
$edit = ['keys' => 'cómmīŦŧęđ BɆĬŇĜ påŔťıçȉpǎǹţș'];
$this->drupalPostForm('search/node', $edit, t('Search'));
$this->assertRaw('<strong>cómmīŦŧęđ</strong>');
$this->assertRaw('<strong>BɆĬŇĜ</strong>');
$this->assertRaw('<strong>påŔťıçȉpǎǹţș</strong>');
$edit = ['keys' => 'committed being participants'];
$this->drupalPostForm('search/node', $edit, t('Search'));
$this->assertRaw('<strong>cómmīŦŧęđ</strong>');
$this->assertRaw('<strong>BɆĬŇĜ</strong>');
$this->assertRaw('<strong>påŔťıçȉpǎǹţș</strong>');
$edit = ['keys' => 'Enricþment'];
$this->drupalPostForm('search/node', $edit, t('Search'));
$this->assertRaw('<strong>Enricþment</strong>');
$edit = ['keys' => 'Enritchment'];
$this->drupalPostForm('search/node', $edit, t('Search'));
$this->assertNoRaw('<strong>Enricþment</strong>');
$edit = ['keys' => 'æll'];
$this->drupalPostForm('search/node', $edit, t('Search'));
$this->assertRaw('<strong>æll</strong>');
$edit = ['keys' => 'all'];
$this->drupalPostForm('search/node', $edit, t('Search'));
$this->assertNoRaw('<strong>æll</strong>');
}
}

View file

@ -0,0 +1,72 @@
<?php
namespace Drupal\Tests\search\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests search functionality with punctuation and HTML entities.
*
* @group search
*/
class SearchNodePunctuationTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['node', 'search'];
/**
* A user with permission to use advanced search.
*
* @var \Drupal\user\UserInterface
*/
public $testUser;
protected function setUp() {
parent::setUp();
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
node_access_rebuild();
// Create a test user and log in.
$this->testUser = $this->drupalCreateUser(['access content', 'search content', 'use advanced search', 'access user profiles']);
$this->drupalLogin($this->testUser);
}
/**
* Tests that search works with punctuation and HTML entities.
*/
public function testPhraseSearchPunctuation() {
$node = $this->drupalCreateNode(['body' => [['value' => "The bunny's ears were fluffy."]]]);
$node2 = $this->drupalCreateNode(['body' => [['value' => 'Dignissim Aliquam &amp; Quieligo meus natu quae quia te. Damnum&copy; erat&mdash; neo pneum. Facilisi feugiat ibidem ratis.']]]);
// Update the search index.
$this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex();
search_update_totals();
// Refresh variables after the treatment.
$this->refreshVariables();
// Submit a phrase wrapped in double quotes to include the punctuation.
$edit = ['keys' => '"bunny\'s"'];
$this->drupalPostForm('search/node', $edit, t('Search'));
$this->assertText($node->label());
// Check if the author is linked correctly to the user profile page.
$username = $node->getOwner()->getUsername();
$this->assertLink($username);
// Search for "&" and verify entities are not broken up in the output.
$edit = ['keys' => '&'];
$this->drupalPostForm('search/node', $edit, t('Search'));
$this->assertNoRaw('<strong>&</strong>amp;');
$this->assertText('You must include at least one keyword');
$edit = ['keys' => '&amp;'];
$this->drupalPostForm('search/node', $edit, t('Search'));
$this->assertNoRaw('<strong>&</strong>amp;');
$this->assertText('You must include at least one keyword');
}
}

View file

@ -0,0 +1,110 @@
<?php
namespace Drupal\Tests\search\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests search index is updated properly when nodes are removed or updated.
*
* @group search
*/
class SearchNodeUpdateAndDeletionTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['node', 'search'];
/**
* A user with permission to access and search content.
*
* @var \Drupal\user\UserInterface
*/
public $testUser;
protected function setUp() {
parent::setUp();
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
// Create a test user and log in.
$this->testUser = $this->drupalCreateUser(['access content', 'search content']);
$this->drupalLogin($this->testUser);
}
/**
* Tests that the search index info is properly updated when a node changes.
*/
public function testSearchIndexUpdateOnNodeChange() {
// Create a node.
$node = $this->drupalCreateNode([
'title' => 'Someone who says Ni!',
'body' => [['value' => "We are the knights who say Ni!"]],
'type' => 'page',
]);
$node_search_plugin = $this->container->get('plugin.manager.search')->createInstance('node_search');
// Update the search index.
$node_search_plugin->updateIndex();
search_update_totals();
// Search the node to verify it appears in search results
$edit = ['keys' => 'knights'];
$this->drupalPostForm('search/node', $edit, t('Search'));
$this->assertText($node->label());
// Update the node
$node->body->value = "We want a shrubbery!";
$node->save();
// Run indexer again
$node_search_plugin->updateIndex();
search_update_totals();
// Search again to verify the new text appears in test results.
$edit = ['keys' => 'shrubbery'];
$this->drupalPostForm('search/node', $edit, t('Search'));
$this->assertText($node->label());
}
/**
* Tests that the search index info is updated when a node is deleted.
*/
public function testSearchIndexUpdateOnNodeDeletion() {
// Create a node.
$node = $this->drupalCreateNode([
'title' => 'No dragons here',
'body' => [['value' => 'Again: No dragons here']],
'type' => 'page',
]);
$node_search_plugin = $this->container->get('plugin.manager.search')->createInstance('node_search');
// Update the search index.
$node_search_plugin->updateIndex();
search_update_totals();
// Search the node to verify it appears in search results
$edit = ['keys' => 'dragons'];
$this->drupalPostForm('search/node', $edit, t('Search'));
$this->assertText($node->label());
// Get the node info from the search index tables.
$search_index_dataset = db_query("SELECT sid FROM {search_index} WHERE type = 'node_search' AND word = :word", [':word' => 'dragons'])
->fetchField();
$this->assertNotEqual($search_index_dataset, FALSE, t('Node info found on the search_index'));
// Delete the node.
$node->delete();
// Check if the node info is gone from the search table.
$search_index_dataset = db_query("SELECT sid FROM {search_index} WHERE type = 'node_search' AND word = :word", [':word' => 'dragons'])
->fetchField();
$this->assertFalse($search_index_dataset, t('Node info successfully removed from search_index'));
// Search again to verify the node doesn't appear anymore.
$this->drupalPostForm('search/node', $edit, t('Search'));
$this->assertNoText($node->label());
}
}

View file

@ -0,0 +1,111 @@
<?php
namespace Drupal\Tests\search\Functional;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\Traits\Core\CronRunTrait;
/**
* Tests that numbers can be searched with more complex matching.
*
* @group search
*/
class SearchNumberMatchingTest extends BrowserTestBase {
use CronRunTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['dblog', 'node', 'search'];
/**
* A user with permission to administer nodes.
*
* @var \Drupal\user\UserInterface
*/
protected $testUser;
/**
* An array of strings containing numbers to use for testing.
*
* Define a group of numbers that should all match each other --
* numbers with internal punctuation should match each other, as well
* as numbers with and without leading zeros and leading/trailing
* . and -.
*
* @var string[]
*/
protected $numbers = [
'123456789',
'12/34/56789',
'12.3456789',
'12-34-56789',
'123,456,789',
'-123456789',
'0123456789',
];
/**
* An array of nodes created for testing purposes.
*
* @var \Drupal\node\NodeInterface[]
*/
protected $nodes;
protected function setUp() {
parent::setUp();
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
$this->testUser = $this->drupalCreateUser(['search content', 'access content', 'administer nodes', 'access site reports']);
$this->drupalLogin($this->testUser);
foreach ($this->numbers as $num) {
$info = [
'body' => [['value' => $num]],
'type' => 'page',
'language' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
];
$this->nodes[] = $this->drupalCreateNode($info);
}
// Run cron to ensure the content is indexed.
$this->cronRun();
$this->drupalGet('admin/reports/dblog');
$this->assertText(t('Cron run completed'), 'Log shows cron run completed');
}
/**
* Tests that all the numbers can be searched.
*/
public function testNumberSearching() {
for ($i = 0; $i < count($this->numbers); $i++) {
$node = $this->nodes[$i];
// Verify that the node title does not appear on the search page
// with a dummy search.
$this->drupalPostForm('search/node',
['keys' => 'foo'],
t('Search'));
$this->assertNoText($node->label(), format_string('%number: node title not shown in dummy search', ['%number' => $i]));
// Now verify that we can find node i by searching for any of the
// numbers.
for ($j = 0; $j < count($this->numbers); $j++) {
$number = $this->numbers[$j];
// If the number is negative, remove the - sign, because - indicates
// "not keyword" when searching.
$number = ltrim($number, '-');
$this->drupalPostForm('search/node',
['keys' => $number],
t('Search'));
$this->assertText($node->label(), format_string('%i: node title shown (search found the node) in search for number %number', ['%i' => $i, '%number' => $number]));
}
}
}
}

View file

@ -0,0 +1,116 @@
<?php
namespace Drupal\Tests\search\Functional;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\Traits\Core\CronRunTrait;
/**
* Tests that numbers can be searched.
*
* @group search
*/
class SearchNumbersTest extends BrowserTestBase {
use CronRunTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['dblog', 'node', 'search'];
/**
* A user with permission to administer nodes.
*
* @var \Drupal\user\UserInterface
*/
protected $testUser;
/**
* An array containing a series of "numbers" for testing purposes.
*
* Create content with various numbers in it.
* Note: 50 characters is the current limit of the search index's word
* field.
*
* @var string[]
*/
protected $numbers = [
'ISBN' => '978-0446365383',
'UPC' => '036000 291452',
'EAN bar code' => '5901234123457',
'negative' => '-123456.7890',
'quoted negative' => '"-123456.7890"',
'leading zero' => '0777777777',
'tiny' => '111',
'small' => '22222222222222',
'medium' => '333333333333333333333333333',
'large' => '444444444444444444444444444444444444444',
'gigantic' => '5555555555555555555555555555555555555555555555555',
'over fifty characters' => '666666666666666666666666666666666666666666666666666666666666',
'date' => '01/02/2009',
'commas' => '987,654,321',
];
/**
* An array of nodes created for testing purposes.
*
* @var \Drupal\node\NodeInterface[]
*/
protected $nodes;
protected function setUp() {
parent::setUp();
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
$this->testUser = $this->drupalCreateUser(['search content', 'access content', 'administer nodes', 'access site reports']);
$this->drupalLogin($this->testUser);
foreach ($this->numbers as $doc => $num) {
$info = [
'body' => [['value' => $num]],
'type' => 'page',
'language' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
'title' => $doc . ' number',
];
$this->nodes[$doc] = $this->drupalCreateNode($info);
}
// Run cron to ensure the content is indexed.
$this->cronRun();
$this->drupalGet('admin/reports/dblog');
$this->assertText(t('Cron run completed'), 'Log shows cron run completed');
}
/**
* Tests that all the numbers can be searched.
*/
public function testNumberSearching() {
$types = array_keys($this->numbers);
foreach ($types as $type) {
$number = $this->numbers[$type];
// If the number is negative, remove the - sign, because - indicates
// "not keyword" when searching.
$number = ltrim($number, '-');
$node = $this->nodes[$type];
// Verify that the node title does not appear on the search page
// with a dummy search.
$this->drupalPostForm('search/node',
['keys' => 'foo'],
t('Search'));
$this->assertNoText($node->label(), $type . ': node title not shown in dummy search');
// Verify that the node title does appear as a link on the search page
// when searching for the number.
$this->drupalPostForm('search/node',
['keys' => $number],
t('Search'));
$this->assertText($node->label(), format_string('%type: node title shown (search found the node) in search for number %number.', ['%type' => $type, '%number' => $number]));
}
}
}

View file

@ -0,0 +1,226 @@
<?php
namespace Drupal\Tests\search\Functional;
use Drupal\Core\Cache\Cache;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait;
/**
* Tests the search_page entity cache tags on the search results pages.
*
* @group search
*/
class SearchPageCacheTagsTest extends BrowserTestBase {
use AssertPageCacheContextsAndTagsTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['node', 'search'];
/**
* {@inheritdoc}
*/
protected $dumpHeaders = TRUE;
/**
* A user with permission to search content.
*
* @var \Drupal\user\UserInterface
*/
protected $searchingUser;
/**
* A node that is indexed by the search module.
*
* @var \Drupal\node\NodeInterface
*/
protected $node;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
// Create user.
$this->searchingUser = $this->drupalCreateUser(['search content', 'access user profiles']);
// Create a node and update the search index.
$this->node = $this->drupalCreateNode(['title' => 'bike shed shop']);
$this->node->setOwner($this->searchingUser);
$this->node->save();
$this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex();
search_update_totals();
}
/**
* Tests the presence of the expected cache tag in various situations.
*/
public function testSearchText() {
$this->drupalLogin($this->searchingUser);
// Initial page for searching nodes.
$this->drupalGet('search/node');
$this->assertCacheTag('config:search.page.node_search');
$this->assertCacheTag('search_index:node_search');
$this->assertCacheTag('node_list');
// Node search results.
$edit = [];
$edit['keys'] = 'bike shed';
$this->drupalPostForm('search/node', $edit, t('Search'));
$this->assertText('bike shed shop');
$this->assertCacheTag('config:search.page.node_search');
$this->assertCacheTag('search_index');
$this->assertCacheTag('search_index:node_search');
$this->assertCacheTag('node:1');
$this->assertCacheTag('user:2');
$this->assertCacheTag('rendered');
$this->assertCacheTag('http_response');
$this->assertCacheTag('node_list');
// Updating a node should invalidate the search plugin's index cache tag.
$this->node->title = 'bike shop';
$this->node->save();
$this->drupalPostForm('search/node', $edit, t('Search'));
$this->assertText('bike shop');
$this->assertCacheTag('config:search.page.node_search');
$this->assertCacheTag('search_index');
$this->assertCacheTag('search_index:node_search');
$this->assertCacheTag('node:1');
$this->assertCacheTag('user:2');
$this->assertCacheTag('rendered');
$this->assertCacheTag('http_response');
$this->assertCacheTag('node_list');
// Deleting a node should invalidate the search plugin's index cache tag.
$this->node->delete();
$this->drupalPostForm('search/node', $edit, t('Search'));
$this->assertText('Your search yielded no results.');
$this->assertCacheTag('config:search.page.node_search');
$this->assertCacheTag('search_index');
$this->assertCacheTag('search_index:node_search');
$this->assertCacheTag('node_list');
// Initial page for searching users.
$this->drupalGet('search/user');
$this->assertCacheTag('config:search.page.user_search');
$this->assertCacheTag('user_list');
$this->assertSession()->responseHeaderNotContains('X-Drupal-Cache-Tags', 'search_index');
$this->assertSession()->responseHeaderNotContains('X-Drupal-Cache-Tags', 'search_index:user_search');
// User search results.
$edit['keys'] = $this->searchingUser->getUsername();
$this->drupalPostForm('search/user', $edit, t('Search'));
$this->assertCacheTag('config:search.page.user_search');
$this->assertCacheTag('user_list');
$this->assertCacheTag('user:2');
$this->assertSession()->responseHeaderNotContains('X-Drupal-Cache-Tags', 'search_index');
$this->assertSession()->responseHeaderNotContains('X-Drupal-Cache-Tags', 'search_index:user_search');
}
/**
* Tests the presence of expected cache tags with referenced entities.
*/
public function testSearchTagsBubbling() {
// Install field UI and entity reference modules.
$this->container->get('module_installer')->install(['field_ui', 'entity_reference']);
$this->resetAll();
// Creates a new content type that will have an entity reference.
$type_name = 'entity_reference_test';
$type = $this->drupalCreateContentType(['name' => $type_name, 'type' => $type_name]);
$bundle_path = 'admin/structure/types/manage/' . $type->id();
// Create test user.
$admin_user = $this->drupalCreateUser([
'access content',
'create ' . $type_name . ' content',
'administer node fields',
'administer node display',
]);
$this->drupalLogin($admin_user);
// First step: 'Add new field' on the 'Manage fields' page.
$this->drupalGet($bundle_path . '/fields/add-field');
$this->drupalPostForm(NULL, [
'label' => 'Test label',
'field_name' => 'test__ref',
'new_storage_type' => 'entity_reference',
], t('Save and continue'));
// Second step: 'Field settings' form.
$this->drupalPostForm(NULL, [], t('Save field settings'));
// Create a new node of our newly created node type and fill in the entity
// reference field.
$edit = [
'title[0][value]' => 'Llama shop',
'field_test__ref[0][target_id]' => $this->node->getTitle(),
];
$this->drupalPostForm('node/add/' . $type->id(), $edit, t('Save'));
// Test that the value of the entity reference field is shown.
$this->drupalGet('node/2');
$this->assertText('bike shed shop');
// Refresh the search index.
$this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex();
search_update_totals();
// Log in with searching user again.
$this->drupalLogin($this->searchingUser);
// Default search cache tags.
$default_search_tags = [
'config:search.page.node_search',
'search_index',
'search_index:node_search',
'http_response',
'rendered',
'node_list',
];
// Node search results for shop, should return node:1 (bike shed shop) and
// node:2 (Llama shop). The related authors cache tags should be visible as
// well.
$edit = [];
$edit['keys'] = 'shop';
$this->drupalPostForm('search/node', $edit, t('Search'));
$this->assertText('bike shed shop');
$this->assertText('Llama shop');
$expected_cache_tags = Cache::mergeTags($default_search_tags, [
'node:1',
'user:2',
'node:2',
'user:3',
'node_view',
'config:filter.format.plain_text',
]);
$this->assertCacheTags($expected_cache_tags);
// Only get the new node in the search results, should result in node:1,
// node:2 and user:3 as cache tags even though only node:1 is shown. This is
// because node:2 is reference in node:1 as an entity reference.
$edit = [];
$edit['keys'] = 'Llama';
$this->drupalPostForm('search/node', $edit, t('Search'));
$this->assertText('Llama shop');
$expected_cache_tags = Cache::mergeTags($default_search_tags, [
'node:1',
'node:2',
'user:3',
'node_view',
]);
$this->assertCacheTags($expected_cache_tags);
}
}

View file

@ -0,0 +1,44 @@
<?php
namespace Drupal\Tests\search\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests if the result page can be overridden.
*
* Verifies that a plugin can override the buildResults() method to
* control what the search results page looks like.
*
* @group search
*/
class SearchPageOverrideTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['search', 'search_extra_type'];
/**
* A user with permission to administer search.
*
* @var \Drupal\user\UserInterface
*/
public $searchUser;
protected function setUp() {
parent::setUp();
// Log in as a user that can create and search content.
$this->searchUser = $this->drupalCreateUser(['search content', 'administer search']);
$this->drupalLogin($this->searchUser);
}
public function testSearchPageHook() {
$keys = 'bike shed ' . $this->randomMachineName();
$this->drupalGet("search/dummy_path", ['query' => ['keys' => $keys]]);
$this->assertText('Dummy search snippet', 'Dummy search snippet is shown');
$this->assertText('Test page text is here', 'Page override is working');
}
}

View file

@ -0,0 +1,165 @@
<?php
namespace Drupal\Tests\search\Functional;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\Unicode;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the search help text and search page text.
*
* @group search
*/
class SearchPageTextTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['block', 'node', 'search'];
/**
* A user with permission to use advanced search.
*
* @var \Drupal\user\UserInterface
*/
protected $searchingUser;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
// Create user.
$this->searchingUser = $this->drupalCreateUser(['search content', 'access user profiles', 'use advanced search']);
$this->drupalPlaceBlock('local_tasks_block');
$this->drupalPlaceBlock('page_title_block');
}
/**
* Tests for XSS in search module local task.
*
* This is a regression test for https://www.drupal.org/node/2338081
*/
public function testSearchLabelXSS() {
$this->drupalLogin($this->drupalCreateUser(['administer search']));
$keys['label'] = '<script>alert("Dont Panic");</script>';
$this->drupalPostForm('admin/config/search/pages/manage/node_search', $keys, t('Save search page'));
$this->drupalLogin($this->searchingUser);
$this->drupalGet('search/node');
$this->assertEscaped($keys['label']);
}
/**
* Tests the failed search text, and various other text on the search page.
*/
public function testSearchText() {
$this->drupalLogin($this->searchingUser);
$this->drupalGet('search/node');
$this->assertText(t('Enter your keywords'));
$this->assertText(t('Search'));
$this->assertTitle(t('Search') . ' | Drupal', 'Search page title is correct');
$edit = [];
$search_terms = 'bike shed ' . $this->randomMachineName();
$edit['keys'] = $search_terms;
$this->drupalPostForm('search/node', $edit, t('Search'));
$this->assertText('search yielded no results');
$this->assertText(t('Search'));
$title_source = 'Search for @keywords | Drupal';
$this->assertTitle(t($title_source, ['@keywords' => Unicode::truncate($search_terms, 60, TRUE, TRUE)]), 'Search page title is correct');
$this->assertNoText('Node', 'Erroneous tab and breadcrumb text is not present');
$this->assertNoText(t('Node'), 'Erroneous translated tab and breadcrumb text is not present');
$this->assertText(t('Content'), 'Tab and breadcrumb text is present');
$this->clickLink('Search help');
$this->assertText('Search help', 'Correct title is on search help page');
$this->assertText('Use upper-case OR to get more results', 'Correct text is on content search help page');
// Search for a longer text, and see that it is in the title, truncated.
$edit = [];
$search_terms = 'Every word is like an unnecessary stain on silence and nothingness.';
$edit['keys'] = $search_terms;
$this->drupalPostForm('search/node', $edit, t('Search'));
$this->assertTitle(t($title_source, ['@keywords' => 'Every word is like an unnecessary stain on silence and…']), 'Search page title is correct');
// Search for a string with a lot of special characters.
$search_terms = 'Hear nothing > "see nothing" `feel' . " '1982.";
$edit['keys'] = $search_terms;
$this->drupalPostForm('search/node', $edit, t('Search'));
$actual_title = $this->xpath('//title')[0]->getText();
$this->assertEqual($actual_title, Html::decodeEntities(t($title_source, ['@keywords' => Unicode::truncate($search_terms, 60, TRUE, TRUE)])), 'Search page title is correct');
$edit['keys'] = $this->searchingUser->getUsername();
$this->drupalPostForm('search/user', $edit, t('Search'));
$this->assertText(t('Search'));
$this->assertTitle(t($title_source, ['@keywords' => Unicode::truncate($this->searchingUser->getUsername(), 60, TRUE, TRUE)]));
$this->clickLink('Search help');
$this->assertText('Search help', 'Correct title is on search help page');
$this->assertText('user names and partial user names', 'Correct text is on user search help page');
// Test that search keywords containing slashes are correctly loaded
// from the GET params and displayed in the search form.
$arg = $this->randomMachineName() . '/' . $this->randomMachineName();
$this->drupalGet('search/node', ['query' => ['keys' => $arg]]);
$input = $this->xpath("//input[@id='edit-keys' and @value='{$arg}']");
$this->assertFalse(empty($input), 'Search keys with a / are correctly set as the default value in the search box.');
// Test a search input exceeding the limit of AND/OR combinations to test
// the Denial-of-Service protection.
$limit = $this->config('search.settings')->get('and_or_limit');
$keys = [];
for ($i = 0; $i < $limit + 1; $i++) {
// Use a key of 4 characters to ensure we never generate 'AND' or 'OR'.
$keys[] = $this->randomMachineName(4);
if ($i % 2 == 0) {
$keys[] = 'OR';
}
}
$edit['keys'] = implode(' ', $keys);
$this->drupalPostForm('search/node', $edit, t('Search'));
$this->assertRaw(t('Your search used too many AND/OR expressions. Only the first @count terms were included in this search.', ['@count' => $limit]));
// Test that a search on Node or User with no keywords entered generates
// the "Please enter some keywords" message.
$this->drupalPostForm('search/node', [], t('Search'));
$this->assertText(t('Please enter some keywords'), 'With no keywords entered, message is displayed on node page');
$this->drupalPostForm('search/user', [], t('Search'));
$this->assertText(t('Please enter some keywords'), 'With no keywords entered, message is displayed on user page');
// Make sure the "Please enter some keywords" message is NOT displayed if
// you use "or" words or phrases in Advanced Search.
$this->drupalPostForm('search/node', ['or' => $this->randomMachineName() . ' ' . $this->randomMachineName()], 'edit-submit--2');
$this->assertNoText(t('Please enter some keywords'), 'With advanced OR keywords entered, no keywords message is not displayed on node page');
$this->drupalPostForm('search/node', ['phrase' => '"' . $this->randomMachineName() . '" "' . $this->randomMachineName() . '"'], 'edit-submit--2');
$this->assertNoText(t('Please enter some keywords'), 'With advanced phrase entered, no keywords message is not displayed on node page');
// Verify that if you search for a too-short keyword, you get the right
// message, and that if after that you search for a longer keyword, you
// do not still see the message.
$this->drupalPostForm('search/node', ['keys' => $this->randomMachineName(1)], t('Search'));
$this->assertText('You must include at least one keyword', 'Keyword message is displayed when searching for short word');
$this->assertNoText(t('Please enter some keywords'), 'With short word entered, no keywords message is not displayed');
$this->drupalPostForm(NULL, ['keys' => $this->randomMachineName()], t('Search'));
$this->assertNoText('You must include at least one keyword', 'Keyword message is not displayed when searching for long word after short word search');
// Test that if you search for a URL with .. in it, you still end up at
// the search page. See issue https://www.drupal.org/node/890058.
$this->drupalPostForm('search/node', ['keys' => '../../admin'], t('Search'));
$this->assertResponse(200, 'Searching for ../../admin with non-admin user does not lead to a 403 error');
$this->assertText('no results', 'Searching for ../../admin with non-admin user gives you a no search results page');
// Test that if you search for a URL starting with "./", you still end up
// at the search page. See issue https://www.drupal.org/node/1421560.
$this->drupalPostForm('search/node', ['keys' => '.something'], t('Search'));
$this->assertResponse(200, 'Searching for .something does not lead to a 403 error');
$this->assertText('no results', 'Searching for .something gives you a no search results page');
}
}

View file

@ -0,0 +1,101 @@
<?php
namespace Drupal\Tests\search\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests that the search preprocessing uses the correct language code.
*
* @group search
*/
class SearchPreprocessLangcodeTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['node', 'search', 'search_langcode_test'];
/**
* Test node for searching.
*
* @var \Drupal\node\NodeInterface
*/
protected $node;
protected function setUp() {
parent::setUp();
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
$web_user = $this->drupalCreateUser([
'create page content',
'edit own page content',
'search content',
'use advanced search',
]);
$this->drupalLogin($web_user);
}
/**
* Tests that hook_search_preprocess() returns the correct langcode.
*/
public function testPreprocessLangcode() {
// Create a node.
$this->node = $this->drupalCreateNode(['body' => [[]], 'langcode' => 'en']);
// First update the index. This does the initial processing.
$this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex();
// Then, run the shutdown function. Testing is a unique case where indexing
// and searching has to happen in the same request, so running the shutdown
// function manually is needed to finish the indexing process.
search_update_totals();
// Search for the additional text that is added by the preprocess
// function. If you search for text that is in the node, preprocess is
// not invoked on the node during the search excerpt generation.
$edit = ['or' => 'Additional text'];
$this->drupalPostForm('search/node', $edit, 'edit-submit--2');
// Checks if the langcode message has been set by hook_search_preprocess().
$this->assertText('Langcode Preprocess Test: en');
}
/**
* Tests stemming for hook_search_preprocess().
*/
public function testPreprocessStemming() {
// Create a node.
$this->node = $this->drupalCreateNode([
'title' => 'we are testing',
'body' => [[]],
'langcode' => 'en',
]);
// First update the index. This does the initial processing.
$this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex();
// Then, run the shutdown function. Testing is a unique case where indexing
// and searching has to happen in the same request, so running the shutdown
// function manually is needed to finish the indexing process.
search_update_totals();
// Search for the title of the node with a POST query.
$edit = ['or' => 'testing'];
$this->drupalPostForm('search/node', $edit, 'edit-submit--2');
// Check if the node has been found.
$this->assertText('Search results');
$this->assertText('we are testing');
// Search for the same node using a different query.
$edit = ['or' => 'test'];
$this->drupalPostForm('search/node', $edit, 'edit-submit--2');
// Check if the node has been found.
$this->assertText('Search results');
$this->assertText('we are testing');
}
}

View file

@ -0,0 +1,53 @@
<?php
namespace Drupal\Tests\search\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests that the node search query can be altered via the query alter hook.
*
* @group search
*/
class SearchQueryAlterTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['node', 'search', 'search_query_alter'];
/**
* Tests that the query alter works.
*/
public function testQueryAlter() {
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
$this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']);
// Log in with sufficient privileges.
$this->drupalLogin($this->drupalCreateUser(['create page content', 'search content']));
// Create a node and an article with the same keyword. The query alter
// test module will alter the query so only articles should be returned.
$data = [
'type' => 'page',
'title' => 'test page',
'body' => [['value' => 'pizza']],
];
$this->drupalCreateNode($data);
$data['type'] = 'article';
$data['title'] = 'test article';
$this->drupalCreateNode($data);
// Update the search index.
$this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex();
search_update_totals();
// Search for the body keyword 'pizza'.
$this->drupalPostForm('search/node', ['keys' => 'pizza'], t('Search'));
// The article should be there but not the page.
$this->assertText('article', 'Article is in search results');
$this->assertNoText('page', 'Page is not in search results');
}
}

View file

@ -0,0 +1,282 @@
<?php
namespace Drupal\Tests\search\Functional;
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
use Drupal\comment\Tests\CommentTestTrait;
use Drupal\Core\Url;
use Drupal\filter\Entity\FilterFormat;
use Drupal\search\Entity\SearchPage;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\Traits\Core\CronRunTrait;
/**
* Indexes content and tests ranking factors.
*
* @group search
*/
class SearchRankingTest extends BrowserTestBase {
use CommentTestTrait;
use CronRunTrait;
/**
* The node search page.
*
* @var \Drupal\search\SearchPageInterface
*/
protected $nodeSearch;
/**
* {@inheritdoc}
*/
protected static $modules = ['node', 'search', 'statistics', 'comment'];
protected function setUp() {
parent::setUp();
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
// Create a plugin instance.
$this->nodeSearch = SearchPage::load('node_search');
// Log in with sufficient privileges.
$this->drupalLogin($this->drupalCreateUser(['post comments', 'skip comment approval', 'create page content', 'administer search']));
}
public function testRankings() {
// Add a comment field.
$this->addDefaultCommentField('node', 'page');
// Build a list of the rankings to test.
$node_ranks = ['sticky', 'promote', 'relevance', 'recent', 'comments', 'views'];
// Create nodes for testing.
$nodes = [];
foreach ($node_ranks as $node_rank) {
$settings = [
'type' => 'page',
'comment' => [
['status' => CommentItemInterface::HIDDEN],
],
'title' => 'Drupal rocks',
'body' => [['value' => "Drupal's search rocks"]],
// Node is one day old.
'created' => REQUEST_TIME - 24 * 3600,
'sticky' => 0,
'promote' => 0,
];
foreach ([0, 1] as $num) {
if ($num == 1) {
switch ($node_rank) {
case 'sticky':
case 'promote':
$settings[$node_rank] = 1;
break;
case 'relevance':
$settings['body'][0]['value'] .= " really rocks";
break;
case 'recent':
// Node is 1 hour hold.
$settings['created'] = REQUEST_TIME - 3600;
break;
case 'comments':
$settings['comment'][0]['status'] = CommentItemInterface::OPEN;
break;
}
}
$nodes[$node_rank][$num] = $this->drupalCreateNode($settings);
}
}
// Add a comment to one of the nodes.
$edit = [];
$edit['subject[0][value]'] = 'my comment title';
$edit['comment_body[0][value]'] = 'some random comment';
$this->drupalGet('comment/reply/node/' . $nodes['comments'][1]->id() . '/comment');
$this->drupalPostForm(NULL, $edit, t('Preview'));
$this->drupalPostForm(NULL, $edit, t('Save'));
// Enable counting of statistics.
$this->config('statistics.settings')->set('count_content_views', 1)->save();
// Simulating content views is kind of difficult in the test. Leave that
// to the Statistics module. So instead go ahead and manually update the
// counter for this node.
$nid = $nodes['views'][1]->id();
db_insert('node_counter')
->fields(['totalcount' => 5, 'daycount' => 5, 'timestamp' => REQUEST_TIME, 'nid' => $nid])
->execute();
// Run cron to update the search index and comment/statistics totals.
$this->cronRun();
// Test that the settings form displays the content ranking section.
$this->drupalGet('admin/config/search/pages/manage/node_search');
$this->assertText(t('Content ranking'));
// Check that all rankings are visible and set to 0.
foreach ($node_ranks as $node_rank) {
$this->assertTrue($this->xpath('//select[@id="edit-rankings-' . $node_rank . '-value"]//option[@value="0"]'), 'Select list to prioritize ' . $node_rank . ' for node ranks is visible and set to 0.');
}
// Test each of the possible rankings.
$edit = [];
foreach ($node_ranks as $node_rank) {
// Enable the ranking we are testing.
$edit['rankings[' . $node_rank . '][value]'] = 10;
$this->drupalPostForm('admin/config/search/pages/manage/node_search', $edit, t('Save search page'));
$this->drupalGet('admin/config/search/pages/manage/node_search');
$this->assertTrue($this->xpath('//select[@id="edit-rankings-' . $node_rank . '-value"]//option[@value="10"]'), 'Select list to prioritize ' . $node_rank . ' for node ranks is visible and set to 10.');
// Reload the plugin to get the up-to-date values.
$this->nodeSearch = SearchPage::load('node_search');
// Do the search and assert the results.
$this->nodeSearch->getPlugin()->setSearch('rocks', [], []);
$set = $this->nodeSearch->getPlugin()->execute();
$this->assertEqual($set[0]['node']->id(), $nodes[$node_rank][1]->id(), 'Search ranking "' . $node_rank . '" order.');
// Clear this ranking for the next test.
$edit['rankings[' . $node_rank . '][value]'] = 0;
}
// Save the final node_rank change then check that all rankings are visible
// and have been set back to 0.
$this->drupalPostForm('admin/config/search/pages/manage/node_search', $edit, t('Save search page'));
$this->drupalGet('admin/config/search/pages/manage/node_search');
foreach ($node_ranks as $node_rank) {
$this->assertTrue($this->xpath('//select[@id="edit-rankings-' . $node_rank . '-value"]//option[@value="0"]'), 'Select list to prioritize ' . $node_rank . ' for node ranks is visible and set to 0.');
}
// Try with sticky, then promoted. This is a test for issue
// https://www.drupal.org/node/771596.
$node_ranks = [
'sticky' => 10,
'promote' => 1,
'relevance' => 0,
'recent' => 0,
'comments' => 0,
'views' => 0,
];
$configuration = $this->nodeSearch->getPlugin()->getConfiguration();
foreach ($node_ranks as $var => $value) {
$configuration['rankings'][$var] = $value;
}
$this->nodeSearch->getPlugin()->setConfiguration($configuration);
$this->nodeSearch->save();
// Do the search and assert the results. The sticky node should show up
// first, then the promoted node, then all the rest.
$this->nodeSearch->getPlugin()->setSearch('rocks', [], []);
$set = $this->nodeSearch->getPlugin()->execute();
$this->assertEqual($set[0]['node']->id(), $nodes['sticky'][1]->id(), 'Search ranking for sticky first worked.');
$this->assertEqual($set[1]['node']->id(), $nodes['promote'][1]->id(), 'Search ranking for promoted second worked.');
// Try with recent, then comments. This is a test for issues
// https://www.drupal.org/node/771596 and
// https://www.drupal.org/node/303574.
$node_ranks = [
'sticky' => 0,
'promote' => 0,
'relevance' => 0,
'recent' => 10,
'comments' => 1,
'views' => 0,
];
$configuration = $this->nodeSearch->getPlugin()->getConfiguration();
foreach ($node_ranks as $var => $value) {
$configuration['rankings'][$var] = $value;
}
$this->nodeSearch->getPlugin()->setConfiguration($configuration);
$this->nodeSearch->save();
// Do the search and assert the results. The recent node should show up
// first, then the commented node, then all the rest.
$this->nodeSearch->getPlugin()->setSearch('rocks', [], []);
$set = $this->nodeSearch->getPlugin()->execute();
$this->assertEqual($set[0]['node']->id(), $nodes['recent'][1]->id(), 'Search ranking for recent first worked.');
$this->assertEqual($set[1]['node']->id(), $nodes['comments'][1]->id(), 'Search ranking for comments second worked.');
}
/**
* Test rankings of HTML tags.
*/
public function testHTMLRankings() {
$full_html_format = FilterFormat::create([
'format' => 'full_html',
'name' => 'Full HTML',
]);
$full_html_format->save();
// Test HTML tags with different weights.
$sorted_tags = ['h1', 'h2', 'h3', 'h4', 'a', 'h5', 'h6', 'notag'];
$shuffled_tags = $sorted_tags;
// Shuffle tags to ensure HTML tags are ranked properly.
shuffle($shuffled_tags);
$settings = [
'type' => 'page',
'title' => 'Simple node',
];
$nodes = [];
foreach ($shuffled_tags as $tag) {
switch ($tag) {
case 'a':
$settings['body'] = [['value' => \Drupal::l('Drupal Rocks', new Url('<front>')), 'format' => 'full_html']];
break;
case 'notag':
$settings['body'] = [['value' => 'Drupal Rocks']];
break;
default:
$settings['body'] = [['value' => "<$tag>Drupal Rocks</$tag>", 'format' => 'full_html']];
break;
}
$nodes[$tag] = $this->drupalCreateNode($settings);
}
// Update the search index.
$this->nodeSearch->getPlugin()->updateIndex();
search_update_totals();
$this->nodeSearch->getPlugin()->setSearch('rocks', [], []);
// Do the search and assert the results.
$set = $this->nodeSearch->getPlugin()->execute();
// Test the ranking of each tag.
foreach ($sorted_tags as $tag_rank => $tag) {
// Assert the results.
if ($tag == 'notag') {
$this->assertEqual($set[$tag_rank]['node']->id(), $nodes[$tag]->id(), 'Search tag ranking for plain text order.');
}
else {
$this->assertEqual($set[$tag_rank]['node']->id(), $nodes[$tag]->id(), 'Search tag ranking for "&lt;' . $sorted_tags[$tag_rank] . '&gt;" order.');
}
}
// Test tags with the same weight against the sorted tags.
$unsorted_tags = ['u', 'b', 'i', 'strong', 'em'];
foreach ($unsorted_tags as $tag) {
$settings['body'] = [['value' => "<$tag>Drupal Rocks</$tag>", 'format' => 'full_html']];
$node = $this->drupalCreateNode($settings);
// Update the search index.
$this->nodeSearch->getPlugin()->updateIndex();
search_update_totals();
$this->nodeSearch->getPlugin()->setSearch('rocks', [], []);
// Do the search and assert the results.
$set = $this->nodeSearch->getPlugin()->execute();
// Ranking should always be second to last.
$set = array_slice($set, -2, 1);
// Assert the results.
$this->assertEqual($set[0]['node']->id(), $node->id(), 'Search tag ranking for "&lt;' . $tag . '&gt;" order.');
// Delete node so it doesn't show up in subsequent search results.
$node->delete();
}
}
}

View file

@ -0,0 +1,53 @@
<?php
namespace Drupal\Tests\search\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests that search works with numeric locale settings.
*
* @group search
*/
class SearchSetLocaleTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['comment', 'node', 'search'];
/**
* A node search plugin instance.
*
* @var \Drupal\search\Plugin\SearchInterface
*/
protected $nodeSearchPlugin;
protected function setUp() {
parent::setUp();
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
// Create a plugin instance.
$this->nodeSearchPlugin = $this->container->get('plugin.manager.search')->createInstance('node_search');
// Create a node with a very simple body.
$this->drupalCreateNode(['body' => [['value' => 'tapir']]]);
// Update the search index.
$this->nodeSearchPlugin->updateIndex();
search_update_totals();
}
/**
* Verify that search works with a numeric locale set.
*/
public function testSearchWithNumericLocale() {
// French decimal point is comma.
setlocale(LC_NUMERIC, 'fr_FR');
$this->nodeSearchPlugin->setSearch('tapir', [], []);
// The call to execute will throw an exception if a float in the wrong
// format is passed in the query to the database, so an assertion is not
// necessary here.
$this->nodeSearchPlugin->execute();
}
}

View file

@ -0,0 +1,88 @@
<?php
namespace Drupal\Tests\search\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests that the search_simply() function works as intended.
*
* @group search
*/
class SearchSimplifyTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['search'];
/**
* Tests that all Unicode characters simplify correctly.
*/
public function testSearchSimplifyUnicode() {
// This test uses a file that was constructed so that the even lines are
// boundary characters, and the odd lines are valid word characters. (It
// was generated as a sequence of all the Unicode characters, and then the
// boundary characters (punctuation, spaces, etc.) were split off into
// their own lines). So the even-numbered lines should simplify to nothing,
// and the odd-numbered lines we need to split into shorter chunks and
// verify that simplification doesn't lose any characters.
$input = file_get_contents($this->root . '/core/modules/search/tests/UnicodeTest.txt');
$basestrings = explode(chr(10), $input);
$strings = [];
foreach ($basestrings as $key => $string) {
if ($key % 2) {
// Even line - should simplify down to a space.
$simplified = search_simplify($string);
$this->assertIdentical($simplified, ' ', "Line $key is excluded from the index");
}
else {
// Odd line, should be word characters.
// Split this into 30-character chunks, so we don't run into limits
// of truncation in search_simplify().
$start = 0;
while ($start < mb_strlen($string)) {
$newstr = mb_substr($string, $start, 30);
// Special case: leading zeros are removed from numeric strings,
// and there's one string in this file that is numbers starting with
// zero, so prepend a 1 on that string.
if (preg_match('/^[0-9]+$/', $newstr)) {
$newstr = '1' . $newstr;
}
$strings[] = $newstr;
$start += 30;
}
}
}
foreach ($strings as $key => $string) {
$simplified = search_simplify($string);
$this->assertTrue(mb_strlen($simplified) >= mb_strlen($string), "Nothing is removed from string $key.");
}
// Test the low-numbered ASCII control characters separately. They are not
// in the text file because they are problematic for diff, especially \0.
$string = '';
for ($i = 0; $i < 32; $i++) {
$string .= chr($i);
}
$this->assertIdentical(' ', search_simplify($string), 'Search simplify works for ASCII control characters.');
}
/**
* Tests that search_simplify() does the right thing with punctuation.
*/
public function testSearchSimplifyPunctuation() {
$cases = [
['20.03/94-28,876', '20039428876', 'Punctuation removed from numbers'],
['great...drupal--module', 'great drupal module', 'Multiple dot and dashes are word boundaries'],
['very_great-drupal.module', 'verygreatdrupalmodule', 'Single dot, dash, underscore are removed'],
['regular,punctuation;word', 'regular punctuation word', 'Punctuation is a word boundary'],
];
foreach ($cases as $case) {
$out = trim(search_simplify($case[0]));
$this->assertEqual($out, $case[1], $case[2]);
}
}
}

View file

@ -0,0 +1,61 @@
<?php
namespace Drupal\Tests\search\Functional;
@trigger_error(__NAMESPACE__ . '\SearchTestBase is deprecated in Drupal 8.6.x and will be removed before Drupal 9.0.0. Instead, use \Drupal\Tests\BrowserTestBase. See https://www.drupal.org/node/2979950.', E_USER_DEPRECATED);
use Drupal\Tests\BrowserTestBase;
/**
* Defines the common search test code.
*
* @deprecated in Drupal 8.6.0 and will be removed in Drupal 9.0.0. Use
* \Drupal\Tests\BrowserTestBase instead.
*
* @see https://www.drupal.org/node/2979950
*/
abstract class SearchTestBase extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['node', 'search', 'dblog'];
protected function setUp() {
parent::setUp();
// Create Basic page and Article node types.
if ($this->profile != 'standard') {
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
$this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']);
}
}
/**
* Submission of a form via press submit button.
*
* @param string $path
* Location of the form to be submitted: either a Drupal path, absolute
* path, or NULL to use the current page.
* @param array $edit
* Form field data to submit. Unlike drupalPostForm(), this does not support
* file uploads.
* @param string $submit
* Value of the submit button to submit clicking. Unlike drupalPostForm(),
* this does not support AJAX.
* @param string $form_html_id
* (optional) HTML ID of the form, to disambiguate.
*
* @deprecated in Drupal 8.6.x, to be removed before Drupal 9.0.x. Use
* \Drupal\Tests\BrowserTestBase::drupalPostForm() instead.
*
* @see https://www.drupal.org/node/2979950
*/
protected function submitGetForm($path, $edit, $submit, $form_html_id = NULL) {
@trigger_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated in Drupal 8.6.x, for removal before the Drupal 9.0.0 release. Use \Drupal\Tests\BrowserTestBase::drupalPostForm() instead. See https://www.drupal.org/node/2979950.', E_USER_DEPRECATED);
$this->drupalPostForm($path, $edit, $submit, [], $form_html_id);
}
}

View file

@ -0,0 +1,157 @@
<?php
namespace Drupal\Tests\search\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests that CJK tokenizer works as intended.
*
* @group search
*/
class SearchTokenizerTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['search'];
/**
* Verifies that strings of CJK characters are tokenized.
*
* The search_simplify() function does special things with numbers, symbols,
* and punctuation. So we only test that CJK characters that are not in these
* character classes are tokenized properly. See PREG_CLASS_CKJ for more
* information.
*/
public function testTokenizer() {
// Set the minimum word size to 1 (to split all CJK characters) and make
// sure CJK tokenizing is turned on.
$this->config('search.settings')
->set('index.minimum_word_size', 1)
->set('index.overlap_cjk', TRUE)
->save();
$this->refreshVariables();
// Create a string of CJK characters from various character ranges in
// the Unicode tables.
// Beginnings of the character ranges.
$starts = [
'CJK unified' => 0x4e00,
'CJK Ext A' => 0x3400,
'CJK Compat' => 0xf900,
'Hangul Jamo' => 0x1100,
'Hangul Ext A' => 0xa960,
'Hangul Ext B' => 0xd7b0,
'Hangul Compat' => 0x3131,
'Half non-punct 1' => 0xff21,
'Half non-punct 2' => 0xff41,
'Half non-punct 3' => 0xff66,
'Hangul Syllables' => 0xac00,
'Hiragana' => 0x3040,
'Katakana' => 0x30a1,
'Katakana Ext' => 0x31f0,
'CJK Reserve 1' => 0x20000,
'CJK Reserve 2' => 0x30000,
'Bomofo' => 0x3100,
'Bomofo Ext' => 0x31a0,
'Lisu' => 0xa4d0,
'Yi' => 0xa000,
];
// Ends of the character ranges.
$ends = [
'CJK unified' => 0x9fcf,
'CJK Ext A' => 0x4dbf,
'CJK Compat' => 0xfaff,
'Hangul Jamo' => 0x11ff,
'Hangul Ext A' => 0xa97f,
'Hangul Ext B' => 0xd7ff,
'Hangul Compat' => 0x318e,
'Half non-punct 1' => 0xff3a,
'Half non-punct 2' => 0xff5a,
'Half non-punct 3' => 0xffdc,
'Hangul Syllables' => 0xd7af,
'Hiragana' => 0x309f,
'Katakana' => 0x30ff,
'Katakana Ext' => 0x31ff,
'CJK Reserve 1' => 0x2fffd,
'CJK Reserve 2' => 0x3fffd,
'Bomofo' => 0x312f,
'Bomofo Ext' => 0x31b7,
'Lisu' => 0xa4fd,
'Yi' => 0xa48f,
];
// Generate characters consisting of starts, midpoints, and ends.
$chars = [];
$charcodes = [];
foreach ($starts as $key => $value) {
$charcodes[] = $starts[$key];
$chars[] = $this->code2utf($starts[$key]);
$mid = round(0.5 * ($starts[$key] + $ends[$key]));
$charcodes[] = $mid;
$chars[] = $this->code2utf($mid);
$charcodes[] = $ends[$key];
$chars[] = $this->code2utf($ends[$key]);
}
// Merge into a string and tokenize.
$string = implode('', $chars);
$out = trim(search_simplify($string));
$expected = mb_strtolower(implode(' ', $chars));
// Verify that the output matches what we expect.
$this->assertEqual($out, $expected, 'CJK tokenizer worked on all supplied CJK characters');
}
/**
* Verifies that strings of non-CJK characters are not tokenized.
*
* This is just a sanity check - it verifies that strings of letters are
* not tokenized.
*/
public function testNoTokenizer() {
// Set the minimum word size to 1 (to split all CJK characters) and make
// sure CJK tokenizing is turned on.
$this->config('search.settings')
->set('index.minimum_word_size', 1)
->set('index.overlap_cjk', TRUE)
->save();
$this->refreshVariables();
$letters = 'abcdefghijklmnopqrstuvwxyz';
$out = trim(search_simplify($letters));
$this->assertEqual($letters, $out, 'Letters are not CJK tokenized');
}
/**
* Like PHP chr() function, but for unicode characters.
*
* Function chr() only works for ASCII characters up to character 255. This
* function converts a number to the corresponding unicode character. Adapted
* from functions supplied in comments on several functions on php.net.
*/
public function code2utf($num) {
if ($num < 128) {
return chr($num);
}
if ($num < 2048) {
return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
}
if ($num < 65536) {
return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
}
if ($num < 2097152) {
return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
}
return '';
}
}

View file

@ -0,0 +1,65 @@
<?php
namespace Drupal\Tests\search\Kernel\Migrate\d6;
use Drupal\Core\Database\Database;
use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
use Drupal\search\Entity\SearchPage;
/**
* Upgrade search rank settings to search.page.*.yml.
*
* @group migrate_drupal_6
*/
class MigrateSearchPageTest extends MigrateDrupal6TestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['search'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->executeMigration('search_page');
}
/**
* Tests Drupal 6 search settings to Drupal 8 search page entity migration.
*/
public function testSearchPage() {
$id = 'node_search';
/** @var \Drupal\search\Entity\SearchPage $search_page */
$search_page = SearchPage::load($id);
$this->assertIdentical($id, $search_page->id());
$configuration = $search_page->getPlugin()->getConfiguration();
$this->assertIdentical($configuration['rankings'], [
'comments' => 5,
'promote' => 0,
'recent' => 0,
'relevance' => 2,
'sticky' => 8,
'views' => 1,
]);
$this->assertIdentical('node', $search_page->getPath());
// Test that we can re-import using the EntitySearchPage destination.
Database::getConnection('default', 'migrate')
->update('variable')
->fields(['value' => serialize(4)])
->condition('name', 'node_rank_comments')
->execute();
/** @var \Drupal\migrate\Plugin\MigrationInterface $migration */
$migration = $this->getMigration('search_page');
// Indicate we're rerunning a migration that's already run.
$migration->getIdMap()->prepareUpdate();
$this->executeMigration($migration);
$configuration = SearchPage::load($id)->getPlugin()->getConfiguration();
$this->assertIdentical(4, $configuration['rankings']['comments']);
}
}

View file

@ -0,0 +1,42 @@
<?php
namespace Drupal\Tests\search\Kernel\Migrate\d6;
use Drupal\Tests\SchemaCheckTestTrait;
use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
/**
* Upgrade variables to search.settings.yml.
*
* @group migrate_drupal_6
*/
class MigrateSearchSettingsTest extends MigrateDrupal6TestBase {
use SchemaCheckTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['search'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->executeMigration('d6_search_settings');
}
/**
* Tests migration of search variables to search.settings.yml.
*/
public function testSearchSettings() {
$config = $this->config('search.settings');
$this->assertIdentical(3, $config->get('index.minimum_word_size'));
$this->assertIdentical(TRUE, $config->get('index.overlap_cjk'));
$this->assertIdentical(100, $config->get('index.cron_limit'));
$this->assertIdentical(TRUE, $config->get('logging'));
$this->assertConfigSchema(\Drupal::service('config.typed'), 'search.settings', $config->get());
}
}

View file

@ -0,0 +1,67 @@
<?php
namespace Drupal\Tests\search\Kernel\Migrate\d7;
use Drupal\Core\Database\Database;
use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase;
use Drupal\search\Entity\SearchPage;
/**
* Upgrade search rank settings to search.page.*.yml.
*
* @group migrate_drupal_7
*/
class MigrateSearchPageTest extends MigrateDrupal7TestBase {
/**
* The modules to be enabled during the test.
*
* {@inheritdoc}
*/
public static $modules = ['node', 'search'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->executeMigration('search_page');
}
/**
* Tests Drupal 7 search ranking to Drupal 8 search page entity migration.
*/
public function testSearchPage() {
$id = 'node_search';
/** @var \Drupal\search\Entity\SearchPage $search_page */
$search_page = SearchPage::load($id);
$this->assertIdentical($id, $search_page->id());
$configuration = $search_page->getPlugin()->getConfiguration();
$expected_rankings = [
'comments' => 0,
'promote' => 0,
'relevance' => 2,
'sticky' => 0,
'views' => 0,
];
$this->assertIdentical($expected_rankings, $configuration['rankings']);
$this->assertIdentical('node', $search_page->getPath());
// Test that we can re-import using the EntitySearchPage destination.
Database::getConnection('default', 'migrate')
->update('variable')
->fields(['value' => serialize(4)])
->condition('name', 'node_rank_comments')
->execute();
/** @var \Drupal\migrate\Plugin\MigrationInterface $migration */
$migration = $this->getMigration('search_page');
// Indicate we're rerunning a migration that's already run.
$migration->getIdMap()->prepareUpdate();
$this->executeMigration($migration);
$configuration = SearchPage::load($id)->getPlugin()->getConfiguration();
$this->assertIdentical(4, $configuration['rankings']['comments']);
}
}

View file

@ -0,0 +1,49 @@
<?php
namespace Drupal\Tests\search\Kernel\Migrate\d7;
use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase;
/**
* Tests migration of Search variables to configuration.
*
* @group search
*/
class MigrateSearchSettingsTest extends MigrateDrupal7TestBase {
public static $modules = ['search'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->executeMigration('d7_search_settings');
}
/**
* Tests the migration of Search's variables to configuration.
*/
public function testSearchSettings() {
$config = $this->config('search.settings');
$this->assertIdentical('node_search', $config->get('default_page'));
$this->assertIdentical(4, $config->get('index.minimum_word_size'));
$this->assertTrue($config->get('index.overlap_cjk'));
$this->assertIdentical(100, $config->get('index.cron_limit'));
$this->assertIdentical(7, $config->get('and_or_limit'));
$this->assertIdentical(25, $config->get('index.tag_weights.h1'));
$this->assertIdentical(18, $config->get('index.tag_weights.h2'));
$this->assertIdentical(15, $config->get('index.tag_weights.h3'));
$this->assertIdentical(12, $config->get('index.tag_weights.h4'));
$this->assertIdentical(9, $config->get('index.tag_weights.h5'));
$this->assertIdentical(6, $config->get('index.tag_weights.h6'));
$this->assertIdentical(3, $config->get('index.tag_weights.u'));
$this->assertIdentical(3, $config->get('index.tag_weights.b'));
$this->assertIdentical(3, $config->get('index.tag_weights.i'));
$this->assertIdentical(3, $config->get('index.tag_weights.strong'));
$this->assertIdentical(3, $config->get('index.tag_weights.em'));
$this->assertIdentical(10, $config->get('index.tag_weights.a'));
$this->assertTrue($config->get('logging'));
}
}

View file

@ -0,0 +1,199 @@
<?php
namespace Drupal\Tests\search\Kernel;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests the search_excerpt() function.
*
* @group search
*/
class SearchExcerptTest extends KernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['search', 'search_langcode_test'];
/**
* Tests search_excerpt() with several simulated search keywords.
*
* Passes keywords and a sample marked up string, "The quick
* brown fox jumps over the lazy dog", and compares it to the
* correctly marked up string. The correctly marked up string
* contains either highlighted keywords or the original marked
* up string if no keywords matched the string.
*/
public function testSearchExcerpt() {
// Make some text with entities and tags.
$text = 'The <strong>quick</strong> <a href="#">brown</a> fox &amp; jumps <h2>over</h2> the lazy dog';
$expected = 'The quick brown fox &amp; jumps over the lazy dog';
$result = $this->doSearchExcerpt('nothing', $text);
$this->assertEqual(preg_replace('| +|', ' ', $result), $expected, 'Entire string, stripped of HTML tags, is returned when keyword is not found in short string');
$result = $this->doSearchExcerpt('fox', $text);
$this->assertEqual($result, 'The quick brown <strong>fox</strong> &amp; jumps over the lazy dog', 'Found keyword is highlighted');
$expected = '<strong>The</strong> quick brown fox &amp; jumps over <strong>the</strong> lazy dog';
$result = $this->doSearchExcerpt('The', $text);
$this->assertEqual(preg_replace('| +|', ' ', $result), $expected, 'Keyword is highlighted at beginning of short string');
$expected = 'The quick brown fox &amp; jumps over the lazy <strong>dog</strong>';
$result = $this->doSearchExcerpt('dog', $text);
$this->assertEqual(preg_replace('| +|', ' ', $result), $expected, 'Keyword is highlighted at end of short string');
$longtext = str_repeat(str_replace('brown', 'silver', $text) . ' ', 10) . $text . str_repeat(' ' . str_replace('brown', 'pink', $text), 10);
$result = $this->doSearchExcerpt('brown', $longtext);
$expected = '… silver fox &amp; jumps over the lazy dog The quick <strong>brown</strong> fox &amp; jumps over the lazy dog The quick …';
$this->assertEqual($result, $expected, 'Snippet around keyword in long text is correctly capped');
$longtext = str_repeat($text . ' ', 10);
$result = $this->doSearchExcerpt('nothing', $longtext);
$expected = 'The quick brown fox &amp; jumps over the lazy dog';
$this->assertTrue(strpos($result, $expected) === 0, 'When keyword is not found in long string, return value starts as expected');
$entities = str_repeat('k&eacute;sz&iacute;t&eacute;se ', 20);
$result = $this->doSearchExcerpt('nothing', $entities);
$this->assertFalse(strpos($result, '&'), 'Entities are not present in excerpt');
$this->assertTrue(strpos($result, 'í') > 0, 'Entities are converted in excerpt');
// The node body that will produce this rendered $text is:
// 123456789 HTMLTest +123456789+&lsquo; +&lsquo; +&lsquo; +&lsquo; +12345678 &nbsp;&nbsp; +&lsquo; +&lsquo; +&lsquo; &lsquo;
$text = "<div class=\"field field--name-body field--type-text-with-summary field--label-hidden\"><div class=\"field__items\"><div class=\"field__item even\" property=\"content:encoded\"><p>123456789 HTMLTest +123456789+ + + + +12345678    + + + </p>\n</div></div></div> ";
$result = $this->doSearchExcerpt('HTMLTest', $text);
$this->assertFalse(empty($result), 'Rendered Multi-byte HTML encodings are not corrupted in search excerpts');
}
/**
* Tests search_excerpt() with search keywords matching simplified words.
*
* Excerpting should handle keywords that are matched only after going through
* search_simplify(). This test passes keywords that match simplified words
* and compares them with strings that contain the original unsimplified word.
*/
public function testSearchExcerptSimplified() {
$start_time = microtime(TRUE);
$lorem1 = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam vitae arcu at leo cursus laoreet. Curabitur dui tortor, adipiscing malesuada tempor in, bibendum ac diam. Cras non tellus a libero pellentesque condimentum. What is a Drupalism? Suspendisse ac lacus libero. Ut non est vel nisl faucibus interdum nec sed leo. Pellentesque sem risus, vulputate eu semper eget, auctor in libero.';
$lorem2 = 'Ut fermentum est vitae metus convallis scelerisque. Phasellus pellentesque rhoncus tellus, eu dignissim purus posuere id. Quisque eu fringilla ligula. Morbi ullamcorper, lorem et mattis egestas, tortor neque pretium velit, eget eleifend odio turpis eu purus. Donec vitae metus quis leo pretium tincidunt a pulvinar sem. Morbi adipiscing laoreet mauris vel placerat. Nullam elementum, nisl sit amet scelerisque malesuada, dolor nunc hendrerit quam, eu ultrices erat est in orci.';
// Make some text with some keywords that will get simplified.
$text = $lorem1 . ' Number: 123456.7890 Hyphenated: one-two abc,def ' . $lorem2;
// Note: The search_excerpt() function adds some extra spaces -- not
// important for HTML formatting. Remove these for comparison.
$result = $this->doSearchExcerpt('123456.7890', $text);
$this->assertTrue(strpos($result, 'Number: <strong>123456.7890</strong>') !== FALSE, 'Numeric keyword is highlighted with exact match');
$result = $this->doSearchExcerpt('1234567890', $text);
$this->assertTrue(strpos($result, 'Number: <strong>123456.7890</strong>') !== FALSE, 'Numeric keyword is highlighted with simplified match');
$result = $this->doSearchExcerpt('Number 1234567890', $text);
$this->assertTrue(strpos($result, '<strong>Number</strong>: <strong>123456.7890</strong>') !== FALSE, 'Punctuated and numeric keyword is highlighted with simplified match');
$result = $this->doSearchExcerpt('"Number 1234567890"', $text);
$this->assertTrue(strpos($result, '<strong>Number: 123456.7890</strong>') !== FALSE, 'Phrase with punctuated and numeric keyword is highlighted with simplified match');
$result = $this->doSearchExcerpt('"Hyphenated onetwo"', $text);
$this->assertTrue(strpos($result, '<strong>Hyphenated: one-two</strong>') !== FALSE, 'Phrase with punctuated and hyphenated keyword is highlighted with simplified match');
$result = $this->doSearchExcerpt('"abc def"', $text);
$this->assertTrue(strpos($result, '<strong>abc,def</strong>') !== FALSE, 'Phrase with keyword simplified into two separate words is highlighted with simplified match');
// Test phrases with characters which are being truncated.
$result = $this->doSearchExcerpt('"ipsum _"', $text);
$this->assertTrue(strpos($result, '<strong>ipsum</strong>') !== FALSE, 'Only valid part of the phrase is highlighted and invalid part containing "_" is ignored.');
$result = $this->doSearchExcerpt('"ipsum 0000"', $text);
$this->assertTrue(strpos($result, '<strong>ipsum</strong>') !== FALSE, 'Only valid part of the phrase is highlighted and invalid part "0000" is ignored.');
// Test combination of the valid keyword and keyword containing only
// characters which are being truncated during simplification.
$result = $this->doSearchExcerpt('ipsum _', $text);
$this->assertTrue(strpos($result, '<strong>ipsum</strong>') !== FALSE, 'Only valid keyword is highlighted and invalid keyword "_" is ignored.');
$result = $this->doSearchExcerpt('ipsum 0000', $text);
$this->assertTrue(strpos($result, '<strong>ipsum</strong>') !== FALSE, 'Only valid keyword is highlighted and invalid keyword "0000" is ignored.');
// Test using the hook_search_preprocess() from the test module.
// The hook replaces "finding" or "finds" with "find".
// So, if we search for "find" or "finds" or "finding", we should
// highlight "finding".
$text = "this tests finding a string";
$result = $this->doSearchExcerpt('finds', $text, 'ex');
$this->assertTrue(strpos($result, '<strong>finding</strong>') !== FALSE, 'Search excerpt works with preprocess hook, search for finds');
$result = $this->doSearchExcerpt('find', $text, 'ex');
$this->assertTrue(strpos($result, '<strong>finding</strong>') !== FALSE, 'Search excerpt works with preprocess hook, search for find');
// Just to be sure, test with the replacement at the beginning and end.
$text = "finding at the beginning";
$result = $this->doSearchExcerpt('finds', $text, 'ex');
$this->assertTrue(strpos($result, '<strong>finding</strong>') !== FALSE, 'Search excerpt works with preprocess hook, text at start');
$text = "at the end finding";
$result = $this->doSearchExcerpt('finds', $text, 'ex');
$this->assertTrue(strpos($result, '<strong>finding</strong>') !== FALSE, 'Search excerpt works with preprocess hook, text at end');
// Testing with a one-to-many replacement: the test module replaces DIC
// with Dependency Injection Container.
$text = "something about the DIC is happening";
$result = $this->doSearchExcerpt('Dependency', $text, 'ex');
$this->assertTrue(strpos($result, '<strong>DIC</strong>') !== FALSE, 'Search excerpt works with preprocess hook, acronym first word');
$result = $this->doSearchExcerpt('Injection', $text, 'ex');
$this->assertTrue(strpos($result, '<strong>DIC</strong>') !== FALSE, 'Search excerpt works with preprocess hook, acronym second word');
$result = $this->doSearchExcerpt('Container', $text, 'ex');
$this->assertTrue(strpos($result, '<strong>DIC</strong>') !== FALSE, 'Search excerpt works with preprocess hook, acronym third word');
// Testing with a many-to-one replacement: the test module replaces
// hypertext markup language with HTML.
$text = "we always use hypertext markup language to describe things";
$result = $this->doSearchExcerpt('html', $text, 'ex');
$this->assertTrue(strpos($result, '<strong>hypertext markup language</strong>') !== FALSE, 'Search excerpt works with preprocess hook, acronym many to one');
// Test with accents and caps in a longer piece of text with the target
// near the end.
$text = str_repeat($lorem2, 20) . ' ' . $lorem1;
$result = $this->doSearchExcerpt('Lìbêró', $text);
$this->assertTrue(strpos($result, '<strong>libero</strong>') !== FALSE, 'Search excerpt works with caps and accents in longer text');
// Test with an acronym provided by the hook, with the target text in the
// middle of a long string.
$text = str_repeat($lorem2, 10) . ' DIC ' . str_repeat($lorem2, 10);
$result = $this->doSearchExcerpt('Dependency', $text, 'ex');
$this->assertTrue(strpos($result, '<strong>DIC</strong>') !== FALSE, 'Search excerpt works with acronym in longer text');
// Test a long string with a lot of whitespace in it.
$lorem3 = str_replace(' ', str_repeat(" \n", 20), $lorem2);
$text = str_repeat($lorem3, 20) . ' ' . $lorem1;
$result = $this->doSearchExcerpt('Lìbêró', $text);
$this->assertTrue(strpos($result, '<strong>libero</strong>') !== FALSE, 'Search excerpt works with caps and accents in longer text with whitespace');
$this->verbose('Elapsed time: ' . (microtime(TRUE) - $start_time));
}
/**
* Calls search_excerpt() and renders output.
*
* @param string $keys
* A string containing a search query.
* @param string $render_array
* The text to extract fragments from.
* @param string|null $langcode
* Language code for the language of $text, if known.
*
* @return string
* A string containing HTML for the excerpt.
*/
protected function doSearchExcerpt($keys, $render_array, $langcode = NULL) {
$render_array = search_excerpt($keys, $render_array, $langcode);
$text = \Drupal::service('renderer')->renderPlain($render_array);
// The search_excerpt() function adds some extra spaces -- not
// important for HTML formatting or this test. Remove these for comparison.
return preg_replace('| +|', ' ', $text);
}
}

View file

@ -0,0 +1,255 @@
<?php
namespace Drupal\Tests\search\Kernel;
use Drupal\Core\Language\LanguageInterface;
use Drupal\KernelTests\KernelTestBase;
/**
* Indexes content and queries it.
*
* @group search
*/
class SearchMatchTest extends KernelTestBase {
// The search index can contain different types of content. Typically the type
// is 'node'. Here we test with _test_ and _test2_ as the type.
const SEARCH_TYPE = '_test_';
const SEARCH_TYPE_2 = '_test2_';
const SEARCH_TYPE_JPN = '_test3_';
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['search'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installSchema('search', ['search_index', 'search_dataset', 'search_total']);
$this->installConfig(['search']);
}
/**
* Test search indexing.
*/
public function testMatching() {
$this->_setup();
$this->_testQueries();
}
/**
* Set up a small index of items to test against.
*/
public function _setup() {
$this->config('search.settings')->set('index.minimum_word_size', 3)->save();
for ($i = 1; $i <= 7; ++$i) {
search_index(static::SEARCH_TYPE, $i, LanguageInterface::LANGCODE_NOT_SPECIFIED, $this->getText($i));
}
for ($i = 1; $i <= 5; ++$i) {
search_index(static::SEARCH_TYPE_2, $i + 7, LanguageInterface::LANGCODE_NOT_SPECIFIED, $this->getText2($i));
}
// No getText builder function for Japanese text; just a simple array.
foreach ([
13 => '以呂波耳・ほへとち。リヌルヲ。',
14 => 'ドルーパルが大好きよ!',
15 => 'コーヒーとケーキ',
] as $i => $jpn) {
search_index(static::SEARCH_TYPE_JPN, $i, LanguageInterface::LANGCODE_NOT_SPECIFIED, $jpn);
}
search_update_totals();
}
/**
* _test_: Helper method for generating snippets of content.
*
* Generated items to test against:
* 1 ipsum
* 2 dolore sit
* 3 sit am ut
* 4 am ut enim am
* 5 ut enim am minim veniam
* 6 enim am minim veniam es cillum
* 7 am minim veniam es cillum dolore eu
*/
public function getText($n) {
$words = explode(' ', "Ipsum dolore sit am. Ut enim am minim veniam. Es cillum dolore eu.");
return implode(' ', array_slice($words, $n - 1, $n));
}
/**
* _test2_: Helper method for generating snippets of content.
*
* Generated items to test against:
* 8 dear
* 9 king philip
* 10 philip came over
* 11 came over from germany
* 12 over from germany swimming
*/
public function getText2($n) {
$words = explode(' ', "Dear King Philip came over from Germany swimming.");
return implode(' ', array_slice($words, $n - 1, $n));
}
/**
* Run predefine queries looking for indexed terms.
*/
public function _testQueries() {
// Note: OR queries that include short words in OR groups are only accepted
// if the ORed terms are ANDed with at least one long word in the rest of
// the query. Examples:
// enim dolore OR ut = enim (dolore OR ut) = (enim dolor) OR (enim ut)
// is good, and
// dolore OR ut = (dolore) OR (ut)
// is bad. This is a design limitation to avoid full table scans.
$queries = [
// Simple AND queries.
'ipsum' => [1],
'enim' => [4, 5, 6],
'xxxxx' => [],
'enim minim' => [5, 6],
'enim xxxxx' => [],
'dolore eu' => [7],
'dolore xx' => [],
'ut minim' => [5],
'xx minim' => [],
'enim veniam am minim ut' => [5],
// Simple OR and AND/OR queries.
'dolore OR ipsum' => [1, 2, 7],
'dolore OR xxxxx' => [2, 7],
'dolore OR ipsum OR enim' => [1, 2, 4, 5, 6, 7],
'ipsum OR dolore sit OR cillum' => [2, 7],
'minim dolore OR ipsum' => [7],
'dolore OR ipsum veniam' => [7],
'minim dolore OR ipsum OR enim' => [5, 6, 7],
'dolore xx OR yy' => [],
'xxxxx dolore OR ipsum' => [],
// Sequence of OR queries.
'minim' => [5, 6, 7],
'minim OR xxxx' => [5, 6, 7],
'minim OR xxxx OR minim' => [5, 6, 7],
'minim OR xxxx minim' => [5, 6, 7],
'minim OR xxxx minim OR yyyy' => [5, 6, 7],
'minim OR xxxx minim OR cillum' => [6, 7, 5],
'minim OR xxxx minim OR xxxx' => [5, 6, 7],
// Negative queries.
'dolore -sit' => [7],
'dolore -eu' => [2],
'dolore -xxxxx' => [2, 7],
'dolore -xx' => [2, 7],
// Phrase queries.
'"dolore sit"' => [2],
'"sit dolore"' => [],
'"am minim veniam es"' => [6, 7],
'"minim am veniam es"' => [],
// Mixed queries.
'"am minim veniam es" OR dolore' => [2, 6, 7],
'"minim am veniam es" OR "dolore sit"' => [2],
'"minim am veniam es" OR "sit dolore"' => [],
'"am minim veniam es" -eu' => [6],
'"am minim veniam" -"cillum dolore"' => [5, 6],
'"am minim veniam" -"dolore cillum"' => [5, 6, 7],
'xxxxx "minim am veniam es" OR dolore' => [],
'xx "minim am veniam es" OR dolore' => [],
];
foreach ($queries as $query => $results) {
$result = db_select('search_index', 'i')
->extend('Drupal\search\SearchQuery')
->searchExpression($query, static::SEARCH_TYPE)
->execute();
$set = $result ? $result->fetchAll() : [];
$this->_testQueryMatching($query, $set, $results);
$this->_testQueryScores($query, $set, $results);
}
// These queries are run against the second index type, SEARCH_TYPE_2.
$queries = [
// Simple AND queries.
'ipsum' => [],
'enim' => [],
'enim minim' => [],
'dear' => [8],
'germany' => [11, 12],
];
foreach ($queries as $query => $results) {
$result = db_select('search_index', 'i')
->extend('Drupal\search\SearchQuery')
->searchExpression($query, static::SEARCH_TYPE_2)
->execute();
$set = $result ? $result->fetchAll() : [];
$this->_testQueryMatching($query, $set, $results);
$this->_testQueryScores($query, $set, $results);
}
// These queries are run against the third index type, SEARCH_TYPE_JPN.
$queries = [
// Simple AND queries.
'呂波耳' => [13],
'以呂波耳' => [13],
'ほへと ヌルヲ' => [13],
'とちリ' => [],
'ドルーパル' => [14],
'パルが大' => [14],
'コーヒー' => [15],
'ヒーキ' => [],
];
foreach ($queries as $query => $results) {
$result = db_select('search_index', 'i')
->extend('Drupal\search\SearchQuery')
->searchExpression($query, static::SEARCH_TYPE_JPN)
->execute();
$set = $result ? $result->fetchAll() : [];
$this->_testQueryMatching($query, $set, $results);
$this->_testQueryScores($query, $set, $results);
}
}
/**
* Test the matching abilities of the engine.
*
* Verify if a query produces the correct results.
*/
public function _testQueryMatching($query, $set, $results) {
// Get result IDs.
$found = [];
foreach ($set as $item) {
$found[] = $item->sid;
}
// Compare $results and $found.
sort($found);
sort($results);
$this->assertEqual($found, $results, "Query matching '$query'");
}
/**
* Test the scoring abilities of the engine.
*
* Verify if a query produces normalized, monotonous scores.
*/
public function _testQueryScores($query, $set, $results) {
// Get result scores.
$scores = [];
foreach ($set as $item) {
$scores[] = $item->calculated_score;
}
// Check order.
$sorted = $scores;
sort($sorted);
$this->assertEqual($scores, array_reverse($sorted), "Query order '$query'");
// Check range.
$this->assertEqual(!count($scores) || (min($scores) > 0.0 && max($scores) <= 1.0001), TRUE, "Query scoring '$query'");
}
}

View file

@ -0,0 +1,287 @@
<?php
/**
* @file
* Contains \Drupal\Tests\search\Unit\SearchPageRepositoryTest.
*/
namespace Drupal\Tests\search\Unit;
use Drupal\search\Entity\SearchPage;
use Drupal\search\SearchPageRepository;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\search\SearchPageRepository
* @group search
*/
class SearchPageRepositoryTest extends UnitTestCase {
/**
* The search page repository.
*
* @var \Drupal\search\SearchPageRepository
*/
protected $searchPageRepository;
/**
* The entity query object.
*
* @var \Drupal\Core\Entity\Query\QueryInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $query;
/**
* The search page storage.
*
* @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $storage;
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $configFactory;
/**
* {@inheritdoc}
*/
protected function setUp() {
$this->query = $this->getMock('Drupal\Core\Entity\Query\QueryInterface');
$this->storage = $this->getMock('Drupal\Core\Config\Entity\ConfigEntityStorageInterface');
$this->storage->expects($this->any())
->method('getQuery')
->will($this->returnValue($this->query));
$entity_manager = $this->getMock('Drupal\Core\Entity\EntityManagerInterface');
$entity_manager->expects($this->any())
->method('getStorage')
->will($this->returnValue($this->storage));
$this->configFactory = $this->getMock('Drupal\Core\Config\ConfigFactoryInterface');
$this->searchPageRepository = new SearchPageRepository($this->configFactory, $entity_manager);
}
/**
* Tests the getActiveSearchPages() method.
*/
public function testGetActiveSearchPages() {
$this->query->expects($this->once())
->method('condition')
->with('status', TRUE)
->will($this->returnValue($this->query));
$this->query->expects($this->once())
->method('execute')
->will($this->returnValue(['test' => 'test', 'other_test' => 'other_test']));
$entities = [];
$entities['test'] = $this->getMock('Drupal\search\SearchPageInterface');
$entities['other_test'] = $this->getMock('Drupal\search\SearchPageInterface');
$this->storage->expects($this->once())
->method('loadMultiple')
->with(['test' => 'test', 'other_test' => 'other_test'])
->will($this->returnValue($entities));
$result = $this->searchPageRepository->getActiveSearchPages();
$this->assertSame($entities, $result);
}
/**
* Tests the isSearchActive() method.
*/
public function testIsSearchActive() {
$this->query->expects($this->once())
->method('condition')
->with('status', TRUE)
->will($this->returnValue($this->query));
$this->query->expects($this->once())
->method('range')
->with(0, 1)
->will($this->returnValue($this->query));
$this->query->expects($this->once())
->method('execute')
->will($this->returnValue(['test' => 'test']));
$this->assertSame(TRUE, $this->searchPageRepository->isSearchActive());
}
/**
* Tests the getIndexableSearchPages() method.
*/
public function testGetIndexableSearchPages() {
$this->query->expects($this->once())
->method('condition')
->with('status', TRUE)
->will($this->returnValue($this->query));
$this->query->expects($this->once())
->method('execute')
->will($this->returnValue(['test' => 'test', 'other_test' => 'other_test']));
$entities = [];
$entities['test'] = $this->getMock('Drupal\search\SearchPageInterface');
$entities['test']->expects($this->once())
->method('isIndexable')
->will($this->returnValue(TRUE));
$entities['other_test'] = $this->getMock('Drupal\search\SearchPageInterface');
$entities['other_test']->expects($this->once())
->method('isIndexable')
->will($this->returnValue(FALSE));
$this->storage->expects($this->once())
->method('loadMultiple')
->with(['test' => 'test', 'other_test' => 'other_test'])
->will($this->returnValue($entities));
$result = $this->searchPageRepository->getIndexableSearchPages();
$this->assertCount(1, $result);
$this->assertSame($entities['test'], reset($result));
}
/**
* Tests the clearDefaultSearchPage() method.
*/
public function testClearDefaultSearchPage() {
$config = $this->getMockBuilder('Drupal\Core\Config\Config')
->disableOriginalConstructor()
->getMock();
$config->expects($this->once())
->method('clear')
->with('default_page')
->will($this->returnValue($config));
$this->configFactory->expects($this->once())
->method('getEditable')
->with('search.settings')
->will($this->returnValue($config));
$this->searchPageRepository->clearDefaultSearchPage();
}
/**
* Tests the getDefaultSearchPage() method when the default is active.
*/
public function testGetDefaultSearchPageWithActiveDefault() {
$this->query->expects($this->once())
->method('condition')
->with('status', TRUE)
->will($this->returnValue($this->query));
$this->query->expects($this->once())
->method('execute')
->will($this->returnValue(['test' => 'test', 'other_test' => 'other_test']));
$config = $this->getMockBuilder('Drupal\Core\Config\Config')
->disableOriginalConstructor()
->getMock();
$config->expects($this->once())
->method('get')
->with('default_page')
->will($this->returnValue('test'));
$this->configFactory->expects($this->once())
->method('get')
->with('search.settings')
->will($this->returnValue($config));
$this->assertSame('test', $this->searchPageRepository->getDefaultSearchPage());
}
/**
* Tests the getDefaultSearchPage() method when the default is inactive.
*/
public function testGetDefaultSearchPageWithInactiveDefault() {
$this->query->expects($this->once())
->method('condition')
->with('status', TRUE)
->will($this->returnValue($this->query));
$this->query->expects($this->once())
->method('execute')
->will($this->returnValue(['test' => 'test']));
$config = $this->getMockBuilder('Drupal\Core\Config\Config')
->disableOriginalConstructor()
->getMock();
$config->expects($this->once())
->method('get')
->with('default_page')
->will($this->returnValue('other_test'));
$this->configFactory->expects($this->once())
->method('get')
->with('search.settings')
->will($this->returnValue($config));
$this->assertSame('test', $this->searchPageRepository->getDefaultSearchPage());
}
/**
* Tests the setDefaultSearchPage() method.
*/
public function testSetDefaultSearchPage() {
$id = 'bananas';
$config = $this->getMockBuilder('Drupal\Core\Config\Config')
->disableOriginalConstructor()
->getMock();
$config->expects($this->once())
->method('set')
->with('default_page', $id)
->will($this->returnValue($config));
$config->expects($this->once())
->method('save')
->will($this->returnValue($config));
$this->configFactory->expects($this->once())
->method('getEditable')
->with('search.settings')
->will($this->returnValue($config));
$search_page = $this->getMock('Drupal\search\SearchPageInterface');
$search_page->expects($this->once())
->method('id')
->will($this->returnValue($id));
$search_page->expects($this->once())
->method('enable')
->will($this->returnValue($search_page));
$search_page->expects($this->once())
->method('save')
->will($this->returnValue($search_page));
$this->searchPageRepository->setDefaultSearchPage($search_page);
}
/**
* Tests the sortSearchPages() method.
*/
public function testSortSearchPages() {
$entity_type = $this->getMock('Drupal\Core\Entity\EntityTypeInterface');
$entity_type->expects($this->any())
->method('getClass')
->will($this->returnValue('Drupal\Tests\search\Unit\TestSearchPage'));
$this->storage->expects($this->once())
->method('getEntityType')
->will($this->returnValue($entity_type));
// Declare entities out of their expected order so we can be sure they were
// sorted. We cannot mock these because of uasort(), see
// https://bugs.php.net/bug.php?id=50688.
$unsorted_entities['test4'] = new TestSearchPage(['weight' => 0, 'status' => FALSE, 'label' => 'Test4']);
$unsorted_entities['test3'] = new TestSearchPage(['weight' => 10, 'status' => TRUE, 'label' => 'Test3']);
$unsorted_entities['test2'] = new TestSearchPage(['weight' => 0, 'status' => TRUE, 'label' => 'Test2']);
$unsorted_entities['test1'] = new TestSearchPage(['weight' => 0, 'status' => TRUE, 'label' => 'Test1']);
$expected = $unsorted_entities;
ksort($expected);
$sorted_entities = $this->searchPageRepository->sortSearchPages($unsorted_entities);
$this->assertSame($expected, $sorted_entities);
}
}
class TestSearchPage extends SearchPage {
public function __construct(array $values) {
foreach ($values as $key => $value) {
$this->$key = $value;
}
}
public function label($langcode = NULL) {
return $this->label;
}
}

View file

@ -0,0 +1,71 @@
<?php
namespace Drupal\Tests\search\Unit;
use Drupal\search\Plugin\SearchPluginCollection;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\search\Plugin\SearchPluginCollection
* @group search
*/
class SearchPluginCollectionTest extends UnitTestCase {
/**
* The mocked plugin manager.
*
* @var \Drupal\Component\Plugin\PluginManagerInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $pluginManager;
/**
* The tested plugin collection.
*
* @var \Drupal\search\Plugin\SearchPluginCollection
*/
protected $searchPluginCollection;
/**
* Stores all setup plugin instances.
*
* @var \Drupal\search\Plugin\SearchInterface[]
*/
protected $pluginInstances;
/**
* {@inheritdoc}
*/
protected function setUp() {
$this->pluginManager = $this->getMock('Drupal\Component\Plugin\PluginManagerInterface');
$this->searchPluginCollection = new SearchPluginCollection($this->pluginManager, 'banana', ['id' => 'banana', 'color' => 'yellow'], 'fruit_stand');
}
/**
* Tests the get() method.
*/
public function testGet() {
$plugin = $this->getMock('Drupal\search\Plugin\SearchInterface');
$this->pluginManager->expects($this->once())
->method('createInstance')
->will($this->returnValue($plugin));
$this->assertSame($plugin, $this->searchPluginCollection->get('banana'));
}
/**
* Tests the get() method with a configurable plugin.
*/
public function testGetWithConfigurablePlugin() {
$plugin = $this->getMock('Drupal\search\Plugin\ConfigurableSearchPluginInterface');
$plugin->expects($this->once())
->method('setSearchPageId')
->with('fruit_stand')
->will($this->returnValue($plugin));
$this->pluginManager->expects($this->once())
->method('createInstance')
->will($this->returnValue($plugin));
$this->assertSame($plugin, $this->searchPluginCollection->get('banana'));
}
}